diff options
Diffstat (limited to 'src/main')
37 files changed, 7455 insertions, 0 deletions
diff --git a/src/main/java/com/zaxxer/hikari/HikariConfig.java b/src/main/java/com/zaxxer/hikari/HikariConfig.java new file mode 100644 index 0000000..ef9f9be --- /dev/null +++ b/src/main/java/com/zaxxer/hikari/HikariConfig.java @@ -0,0 +1,988 @@ +/* + * 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; + +import static com.zaxxer.hikari.util.UtilityElf.getNullIfEmpty; +import static java.util.concurrent.TimeUnit.MINUTES; +import static java.util.concurrent.TimeUnit.SECONDS; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Properties; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadFactory; + +import javax.naming.InitialContext; +import javax.naming.NamingException; +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.metrics.MetricsTrackerFactory; +import com.zaxxer.hikari.util.PropertyElf; + +public class HikariConfig implements HikariConfigMXBean +{ + private static final Logger LOGGER = LoggerFactory.getLogger(HikariConfig.class); + + private static final long CONNECTION_TIMEOUT = SECONDS.toMillis(30); + private static final long VALIDATION_TIMEOUT = SECONDS.toMillis(5); + private static final long IDLE_TIMEOUT = MINUTES.toMillis(10); + private static final long MAX_LIFETIME = MINUTES.toMillis(30); + private static final int DEFAULT_POOL_SIZE = 10; + + private static boolean unitTest; + + // Properties changeable at runtime through the MBean + // + private volatile long connectionTimeout; + private volatile long validationTimeout; + private volatile long idleTimeout; + private volatile long leakDetectionThreshold; + private volatile long maxLifetime; + private volatile int maxPoolSize; + private volatile int minIdle; + + // Properties NOT changeable at runtime + // + private long initializationFailTimeout; + private String catalog; + private String connectionInitSql; + private String connectionTestQuery; + private String dataSourceClassName; + private String dataSourceJndiName; + private String driverClassName; + private String jdbcUrl; + private String password; + private String poolName; + private String transactionIsolationName; + private String username; + private boolean isAutoCommit; + private boolean isReadOnly; + private boolean isIsolateInternalQueries; + private boolean isRegisterMbeans; + private boolean isAllowPoolSuspension; + private DataSource dataSource; + private Properties dataSourceProperties; + private ThreadFactory threadFactory; + private ScheduledExecutorService scheduledExecutor; + private MetricsTrackerFactory metricsTrackerFactory; + private Object metricRegistry; + private Object healthCheckRegistry; + private Properties healthCheckProperties; + + /** + * Default constructor + */ + public HikariConfig() + { + dataSourceProperties = new Properties(); + healthCheckProperties = new Properties(); + + minIdle = -1; + maxPoolSize = -1; + maxLifetime = MAX_LIFETIME; + connectionTimeout = CONNECTION_TIMEOUT; + validationTimeout = VALIDATION_TIMEOUT; + idleTimeout = IDLE_TIMEOUT; + initializationFailTimeout = 1; + isAutoCommit = true; + + String systemProp = System.getProperty("hikaricp.configurationFile"); + if (systemProp != null) { + loadProperties(systemProp); + } + } + + /** + * Construct a HikariConfig from the specified properties object. + * + * @param properties the name of the property file + */ + public HikariConfig(Properties properties) + { + this(); + PropertyElf.setTargetFromProperties(this, properties); + } + + /** + * Construct a HikariConfig from the specified property file name. <code>propertyFileName</code> + * will first be treated as a path in the file-system, and if that fails the + * Class.getResourceAsStream(propertyFileName) will be tried. + * + * @param propertyFileName the name of the property file + */ + public HikariConfig(String propertyFileName) + { + this(); + + loadProperties(propertyFileName); + } + + /** + * Get the default catalog name to be set on connections. + * + * @return the default catalog name + */ + public String getCatalog() + { + return catalog; + } + + /** + * Set the default catalog name to be set on connections. + * + * @param catalog the catalog name, or null + */ + public void setCatalog(String catalog) + { + this.catalog = catalog; + } + + /** + * Get the SQL query to be executed to test the validity of connections. + * + * @return the SQL query string, or null + */ + public String getConnectionTestQuery() + { + return connectionTestQuery; + } + + /** + * Set the SQL query to be executed to test the validity of connections. Using + * the JDBC4 <code>Connection.isValid()</code> method to test connection validity can + * be more efficient on some databases and is recommended. See + * {@link HikariConfig#setJdbc4ConnectionTest(boolean)}. + * + * @param connectionTestQuery a SQL query string + */ + public void setConnectionTestQuery(String connectionTestQuery) + { + this.connectionTestQuery = connectionTestQuery; + } + + /** + * Get the SQL string that will be executed on all new connections when they are + * created, before they are added to the pool. + * + * @return the SQL to execute on new connections, or null + */ + public String getConnectionInitSql() + { + return connectionInitSql; + } + + /** + * Set the SQL string that will be executed on all new connections when they are + * created, before they are added to the pool. If this query fails, it will be + * treated as a failed connection attempt. + * + * @param connectionInitSql the SQL to execute on new connections + */ + public void setConnectionInitSql(String connectionInitSql) + { + this.connectionInitSql = connectionInitSql; + } + + /** {@inheritDoc} */ + @Override + public long getConnectionTimeout() + { + return connectionTimeout; + } + + /** {@inheritDoc} */ + @Override + public void setConnectionTimeout(long connectionTimeoutMs) + { + if (connectionTimeoutMs == 0) { + this.connectionTimeout = Integer.MAX_VALUE; + } + else if (connectionTimeoutMs < 250) { + throw new IllegalArgumentException("connectionTimeout cannot be less than 250ms"); + } + else { + this.connectionTimeout = connectionTimeoutMs; + } + } + + /** {@inheritDoc} */ + @Override + public long getValidationTimeout() + { + return validationTimeout; + } + + /** {@inheritDoc} */ + @Override + public void setValidationTimeout(long validationTimeoutMs) + { + if (validationTimeoutMs < 250) { + throw new IllegalArgumentException("validationTimeout cannot be less than 250ms"); + } + + this.validationTimeout = validationTimeoutMs; + } + + /** + * Get the {@link DataSource} that has been explicitly specified to be wrapped by the + * pool. + * + * @return the {@link DataSource} instance, or null + */ + public DataSource getDataSource() + { + return dataSource; + } + + /** + * Set a {@link DataSource} for the pool to explicitly wrap. This setter is not + * available through property file based initialization. + * + * @param dataSource a specific {@link DataSource} to be wrapped by the pool + */ + public void setDataSource(DataSource dataSource) + { + this.dataSource = dataSource; + } + + public String getDataSourceClassName() + { + return dataSourceClassName; + } + + public void setDataSourceClassName(String className) + { + this.dataSourceClassName = className; + } + + public void addDataSourceProperty(String propertyName, Object value) + { + dataSourceProperties.put(propertyName, value); + } + + public String getDataSourceJNDI() + { + return this.dataSourceJndiName; + } + + public void setDataSourceJNDI(String jndiDataSource) + { + this.dataSourceJndiName = jndiDataSource; + } + + public Properties getDataSourceProperties() + { + return dataSourceProperties; + } + + public void setDataSourceProperties(Properties dsProperties) + { + dataSourceProperties.putAll(dsProperties); + } + + public String getDriverClassName() + { + return driverClassName; + } + + public void setDriverClassName(String driverClassName) + { + try { + Class<?> driverClass = this.getClass().getClassLoader().loadClass(driverClassName); + driverClass.newInstance(); + this.driverClassName = driverClassName; + } + catch (Exception e) { + throw new RuntimeException("Failed to load class of driverClassName " + driverClassName, e); + } + } + + /** {@inheritDoc} */ + @Override + public long getIdleTimeout() + { + return idleTimeout; + } + + /** {@inheritDoc} */ + @Override + public void setIdleTimeout(long idleTimeoutMs) + { + if (idleTimeoutMs < 0) { + throw new IllegalArgumentException("idleTimeout cannot be negative"); + } + this.idleTimeout = idleTimeoutMs; + } + + public String getJdbcUrl() + { + return jdbcUrl; + } + + public void setJdbcUrl(String jdbcUrl) + { + this.jdbcUrl = jdbcUrl; + } + + /** + * Get the default auto-commit behavior of connections in the pool. + * + * @return the default auto-commit behavior of connections + */ + public boolean isAutoCommit() + { + return isAutoCommit; + } + + /** + * Set the default auto-commit behavior of connections in the pool. + * + * @param isAutoCommit the desired auto-commit default for connections + */ + public void setAutoCommit(boolean isAutoCommit) + { + this.isAutoCommit = isAutoCommit; + } + + /** + * Get the pool suspension behavior (allowed or disallowed). + * + * @return the pool suspension behavior + */ + public boolean isAllowPoolSuspension() + { + return isAllowPoolSuspension; + } + + /** + * Set whether or not pool suspension is allowed. There is a performance + * impact when pool suspension is enabled. Unless you need it (for a + * redundancy system for example) do not enable it. + * + * @param isAllowPoolSuspension the desired pool suspension allowance + */ + public void setAllowPoolSuspension(boolean isAllowPoolSuspension) + { + this.isAllowPoolSuspension = isAllowPoolSuspension; + } + + /** + * Get the pool initialization failure timeout. See {@code #setInitializationFailTimeout(long)} + * for details. + * + * @return the number of milliseconds before the pool initialization fails + * @see HikariConfig#setInitializationFailTimeout(long) + */ + public long getInitializationFailTimeout() + { + return initializationFailTimeout; + } + + /** + * Set the pool initialization failure timeout. This setting applies to pool + * initialization when {@link HikariDataSource} is constructed with a {@link HikariConfig}, + * or when {@link HikariDataSource} is constructed using the no-arg constructor + * and {@link HikariDataSource#getConnection()} is called. + * <ul> + * <li>Any value of zero or less will <i>not</i> block the calling thread in the + * case that a connection cannot be obtained. The pool will start and + * continue to try to obtain connections in the background. This can mean + * that callers to {@code DataSource#getConnection()} may encounter + * exceptions.</li> + * <li>Any value greater than zero will be treated as a timeout for pool initialization. + * The calling thread will be blocked from continuing until a successful connection + * to the database, or until the timeout is reached. If the timeout is reached, then + * a {@code PoolInitializationException} will be thrown. + * </ul> + * Note that this timeout does not override the {@code connectionTimeout} or + * {@code validationTimeout}; they will be honored before this timeout is applied. The + * default value is one millisecond. + * + * @param initializationFailTimeout the number of milliseconds before the + * pool initialization fails, or 0 or less to skip the initialization + * check. + */ + public void setInitializationFailTimeout(long initializationFailTimeout) + { + this.initializationFailTimeout = initializationFailTimeout; + } + + /** + * Get whether or not the construction of the pool should throw an exception + * if the minimum number of connections cannot be created. + * + * @return whether or not initialization should fail on error immediately + * @deprecated + */ + @Deprecated + public boolean isInitializationFailFast() + { + return initializationFailTimeout > 0; + } + + /** + * Set whether or not the construction of the pool should throw an exception + * if the minimum number of connections cannot be created. + * + * @param failFast true if the pool should fail if the minimum connections cannot be created + * @deprecated + */ + @Deprecated + public void setInitializationFailFast(boolean failFast) + { + LOGGER.warn("The initializationFailFast propery is deprecated, see initializationFailTimeout"); + + initializationFailTimeout = (failFast ? 1 : 0); + } + + public boolean isIsolateInternalQueries() + { + return isIsolateInternalQueries; + } + + public void setIsolateInternalQueries(boolean isolate) + { + this.isIsolateInternalQueries = isolate; + } + + @Deprecated + public boolean isJdbc4ConnectionTest() + { + return false; + } + + @Deprecated + public void setJdbc4ConnectionTest(boolean useIsValid) + { + LOGGER.warn("The jdbcConnectionTest property is now deprecated, see the documentation for connectionTestQuery"); + } + + public MetricsTrackerFactory getMetricsTrackerFactory() + { + return metricsTrackerFactory; + } + + public void setMetricsTrackerFactory(MetricsTrackerFactory metricsTrackerFactory) + { + if (metricRegistry != null) { + throw new IllegalStateException("cannot use setMetricsTrackerFactory() and setMetricRegistry() together"); + } + + this.metricsTrackerFactory = metricsTrackerFactory; + } + + /** + * Get the Codahale MetricRegistry, could be null. + * + * @return the codahale MetricRegistry instance + */ + public Object getMetricRegistry() + { + return metricRegistry; + } + + /** + * Set a Codahale MetricRegistry to use for HikariCP. + * + * @param metricRegistry the Codahale MetricRegistry to set + */ + public void setMetricRegistry(Object metricRegistry) + { + if (metricsTrackerFactory != null) { + throw new IllegalStateException("cannot use setMetricRegistry() and setMetricsTrackerFactory() together"); + } + + if (metricRegistry != null) { + if (metricRegistry instanceof String) { + try { + InitialContext initCtx = new InitialContext(); + metricRegistry = initCtx.lookup((String) metricRegistry); + } + catch (NamingException e) { + throw new IllegalArgumentException(e); + } + } + + if (!(metricRegistry instanceof MetricRegistry)) { + throw new IllegalArgumentException("Class must be an instance of com.codahale.metrics.MetricRegistry"); + } + } + + this.metricRegistry = metricRegistry; + } + + /** + * Get the Codahale HealthCheckRegistry, could be null. + * + * @return the Codahale HealthCheckRegistry instance + */ + public Object getHealthCheckRegistry() + { + return healthCheckRegistry; + } + + /** + * Set a Codahale HealthCheckRegistry to use for HikariCP. + * + * @param healthCheckRegistry the Codahale HealthCheckRegistry to set + */ + public void setHealthCheckRegistry(Object healthCheckRegistry) + { + if (healthCheckRegistry != null) { + if (healthCheckRegistry instanceof String) { + try { + InitialContext initCtx = new InitialContext(); + healthCheckRegistry = initCtx.lookup((String) healthCheckRegistry); + } + catch (NamingException e) { + throw new IllegalArgumentException(e); + } + } + + if (!(healthCheckRegistry instanceof HealthCheckRegistry)) { + throw new IllegalArgumentException("Class must be an instance of com.codahale.metrics.health.HealthCheckRegistry"); + } + } + + this.healthCheckRegistry = healthCheckRegistry; + } + + public Properties getHealthCheckProperties() + { + return healthCheckProperties; + } + + public void setHealthCheckProperties(Properties healthCheckProperties) + { + this.healthCheckProperties.putAll(healthCheckProperties); + } + + public void addHealthCheckProperty(String key, String value) + { + healthCheckProperties.setProperty(key, value); + } + + public boolean isReadOnly() + { + return isReadOnly; + } + + public void setReadOnly(boolean readOnly) + { + this.isReadOnly = readOnly; + } + + public boolean isRegisterMbeans() + { + return isRegisterMbeans; + } + + public void setRegisterMbeans(boolean register) + { + this.isRegisterMbeans = register; + } + + /** {@inheritDoc} */ + @Override + public long getLeakDetectionThreshold() + { + return leakDetectionThreshold; + } + + /** {@inheritDoc} */ + @Override + public void setLeakDetectionThreshold(long leakDetectionThresholdMs) + { + this.leakDetectionThreshold = leakDetectionThresholdMs; + } + + /** {@inheritDoc} */ + @Override + public long getMaxLifetime() + { + return maxLifetime; + } + + /** {@inheritDoc} */ + @Override + public void setMaxLifetime(long maxLifetimeMs) + { + this.maxLifetime = maxLifetimeMs; + } + + /** {@inheritDoc} */ + @Override + public int getMaximumPoolSize() + { + return maxPoolSize; + } + + /** {@inheritDoc} */ + @Override + public void setMaximumPoolSize(int maxPoolSize) + { + if (maxPoolSize < 1) { + throw new IllegalArgumentException("maxPoolSize cannot be less than 1"); + } + this.maxPoolSize = maxPoolSize; + } + + /** {@inheritDoc} */ + @Override + public int getMinimumIdle() + { + return minIdle; + } + + /** {@inheritDoc} */ + @Override + public void setMinimumIdle(int minIdle) + { + if (minIdle < 0) { + throw new IllegalArgumentException("minimumIdle cannot be negative"); + } + this.minIdle = minIdle; + } + + /** + * Get the default password to use for DataSource.getConnection(username, password) calls. + * @return the password + */ + public String getPassword() + { + return password; + } + + /** + * Set the default password to use for DataSource.getConnection(username, password) calls. + * @param password the password + */ + @Override + public void setPassword(String password) + { + this.password = password; + } + + /** {@inheritDoc} */ + @Override + public String getPoolName() + { + return poolName; + } + + /** + * Set the name of the connection pool. This is primarily used for the MBean + * to uniquely identify the pool configuration. + * + * @param poolName the name of the connection pool to use + */ + public void setPoolName(String poolName) + { + this.poolName = poolName; + } + + /** + * Get the ScheduledExecutorService used for housekeeping. + * + * @return the executor + */ + @Deprecated + public ScheduledThreadPoolExecutor getScheduledExecutorService() + { + return (ScheduledThreadPoolExecutor) scheduledExecutor; + } + + /** + * Set the ScheduledExecutorService used for housekeeping. + * + * @param executor the ScheduledExecutorService + */ + @Deprecated + public void setScheduledExecutorService(ScheduledThreadPoolExecutor executor) + { + this.scheduledExecutor = executor; + } + + /** + * Get the ScheduledExecutorService used for housekeeping. + * + * @return the executor + */ + public ScheduledExecutorService getScheduledExecutor() + { + return scheduledExecutor; + } + + /** + * Set the ScheduledExecutorService used for housekeeping. + * + * @param executor the ScheduledExecutorService + */ + public void setScheduledExecutor(ScheduledExecutorService executor) + { + this.scheduledExecutor = executor; + } + + public String getTransactionIsolation() + { + return transactionIsolationName; + } + + /** + * Set the default transaction isolation level. The specified value is the + * constant name from the <code>Connection</code> class, eg. + * <code>TRANSACTION_REPEATABLE_READ</code>. + * + * @param isolationLevel the name of the isolation level + */ + public void setTransactionIsolation(String isolationLevel) + { + this.transactionIsolationName = isolationLevel; + } + + /** + * Get the default username used for DataSource.getConnection(username, password) calls. + * + * @return the username + */ + public String getUsername() + { + return username; + } + + /** + * Set the default username used for DataSource.getConnection(username, password) calls. + * + * @param username the username + */ + @Override + public void setUsername(String username) + { + this.username = username; + } + + /** + * Get the thread factory used to create threads. + * + * @return the thread factory (may be null, in which case the default thread factory is used) + */ + public ThreadFactory getThreadFactory() + { + return threadFactory; + } + + /** + * Set the thread factory to be used to create threads. + * + * @param threadFactory the thread factory (setting to null causes the default thread factory to be used) + */ + public void setThreadFactory(ThreadFactory threadFactory) + { + this.threadFactory = threadFactory; + } + + public void validate() + { + if (poolName == null) { + poolName = "HikariPool-" + generatePoolNumber(); + } + else if (isRegisterMbeans && poolName.contains(":")) { + throw new IllegalArgumentException("poolName cannot contain ':' when used with JMX"); + } + + // treat empty property as null + catalog = getNullIfEmpty(catalog); + connectionInitSql = getNullIfEmpty(connectionInitSql); + connectionTestQuery = getNullIfEmpty(connectionTestQuery); + transactionIsolationName = getNullIfEmpty(transactionIsolationName); + dataSourceClassName = getNullIfEmpty(dataSourceClassName); + dataSourceJndiName = getNullIfEmpty(dataSourceJndiName); + driverClassName = getNullIfEmpty(driverClassName); + jdbcUrl = getNullIfEmpty(jdbcUrl); + + // Check Data Source Options + if (dataSource != null) { + if (dataSourceClassName != null) { + LOGGER.warn("{} - using dataSource and ignoring dataSourceClassName.", poolName); + } + } + else if (dataSourceClassName != null) { + if (driverClassName != null) { + LOGGER.error("{} - cannot use driverClassName and dataSourceClassName together.", poolName); + // NOTE: This exception text is referenced by a Spring Boot FailureAnalyzer, it should not be + // changed without first notifying the Spring Boot developers. + throw new IllegalStateException("cannot use driverClassName and dataSourceClassName together."); + } + else if (jdbcUrl != null) { + LOGGER.warn("{} - using dataSourceClassName and ignoring jdbcUrl.", poolName); + } + } + else if (jdbcUrl != null) { + } + else if (driverClassName != null) { + LOGGER.error("{} - jdbcUrl is required with driverClassName.", poolName); + throw new IllegalArgumentException("jdbcUrl is required with driverClassName."); + } + else { + LOGGER.error("{} - dataSource or dataSourceClassName or jdbcUrl is required.", poolName); + throw new IllegalArgumentException("dataSource or dataSourceClassName or jdbcUrl is required."); + } + + validateNumerics(); + + if (LOGGER.isDebugEnabled() || unitTest) { + logConfiguration(); + } + } + + private void validateNumerics() + { + if (maxLifetime != 0 && maxLifetime < SECONDS.toMillis(30)) { + LOGGER.warn("{} - maxLifetime is less than 30000ms, setting to default {}ms.", poolName, MAX_LIFETIME); + maxLifetime = MAX_LIFETIME; + } + + if (idleTimeout + SECONDS.toMillis(1) > maxLifetime && maxLifetime > 0) { + LOGGER.warn("{} - idleTimeout is close to or more than maxLifetime, disabling it.", poolName); + idleTimeout = 0; + } + + if (idleTimeout != 0 && idleTimeout < SECONDS.toMillis(10)) { + LOGGER.warn("{} - idleTimeout is less than 10000ms, setting to default {}ms.", poolName, IDLE_TIMEOUT); + idleTimeout = IDLE_TIMEOUT; + } + + if (leakDetectionThreshold > 0 && !unitTest) { + if (leakDetectionThreshold < SECONDS.toMillis(2) || (leakDetectionThreshold > maxLifetime && maxLifetime > 0)) { + LOGGER.warn("{} - leakDetectionThreshold is less than 2000ms or more than maxLifetime, disabling it.", poolName); + leakDetectionThreshold = 0; + } + } + + if (connectionTimeout < 250) { + LOGGER.warn("{} - connectionTimeout is less than 250ms, setting to {}ms.", poolName, CONNECTION_TIMEOUT); + connectionTimeout = CONNECTION_TIMEOUT; + } + + if (validationTimeout < 250) { + LOGGER.warn("{} - validationTimeout is less than 250ms, setting to {}ms.", poolName, VALIDATION_TIMEOUT); + validationTimeout = VALIDATION_TIMEOUT; + } + + if (maxPoolSize < 1) { + maxPoolSize = (minIdle <= 0) ? DEFAULT_POOL_SIZE : minIdle; + } + + if (minIdle < 0 || minIdle > maxPoolSize) { + minIdle = maxPoolSize; + } + } + + private void logConfiguration() + { + LOGGER.debug("{} - configuration:", poolName); + final Set<String> propertyNames = new TreeSet<>(PropertyElf.getPropertyNames(HikariConfig.class)); + for (String prop : propertyNames) { + try { + Object value = PropertyElf.getProperty(prop, this); + if ("dataSourceProperties".equals(prop)) { + Properties dsProps = PropertyElf.copyProperties(dataSourceProperties); + dsProps.setProperty("password", "<masked>"); + value = dsProps; + } + + if ("initializationFailTimeout".equals(prop) && initializationFailTimeout == Long.MAX_VALUE) { + value = "infinite"; + } + else if ("transactionIsolation".equals(prop) && transactionIsolationName == null) { + value = "default"; + } + else if (prop.matches("scheduledExecutorService|threadFactory") && value == null) { + value = "internal"; + } + else if (prop.contains("password")) { + value = "<masked>"; + } + else if (value instanceof String) { + value = "\"" + value + "\""; // quote to see lead/trailing spaces is any + } + else if (value == null) { + value = "none"; + } + LOGGER.debug((prop + "................................................").substring(0, 32) + value); + } + catch (Exception e) { + continue; + } + } + } + + protected void loadProperties(String propertyFileName) + { + final File propFile = new File(propertyFileName); + try (final InputStream is = propFile.isFile() ? new FileInputStream(propFile) : this.getClass().getResourceAsStream(propertyFileName)) { + if (is != null) { + Properties props = new Properties(); + props.load(is); + PropertyElf.setTargetFromProperties(this, props); + } + else { + throw new IllegalArgumentException("Cannot find property file: " + propertyFileName); + } + } + catch (IOException io) { + throw new RuntimeException("Failed to read property file", io); + } + } + + private int generatePoolNumber() + { + // Pool number is global to the VM to avoid overlapping pool numbers in classloader scoped environments + synchronized (System.getProperties()) { + final int next = Integer.getInteger("com.zaxxer.hikari.pool_number", 0) + 1; + System.setProperty("com.zaxxer.hikari.pool_number", String.valueOf(next)); + return next; + } + } + + public void copyState(HikariConfig other) + { + for (Field field : HikariConfig.class.getDeclaredFields()) { + if (!Modifier.isFinal(field.getModifiers())) { + field.setAccessible(true); + try { + field.set(other, field.get(this)); + } + catch (Exception e) { + throw new RuntimeException("Failed to copy HikariConfig state: " + e.getMessage(), e); + } + } + } + } +} diff --git a/src/main/java/com/zaxxer/hikari/HikariConfigMXBean.java b/src/main/java/com/zaxxer/hikari/HikariConfigMXBean.java new file mode 100644 index 0000000..00fefc9 --- /dev/null +++ b/src/main/java/com/zaxxer/hikari/HikariConfigMXBean.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2013 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; + +/** + * The javax.management MBean for a Hikari pool configuration. + * + * @author Brett Wooldridge + */ +public interface HikariConfigMXBean +{ + /** + * Get the maximum number of milliseconds that a client will wait for a connection from the pool. If this + * time is exceeded without a connection becoming available, a SQLException will be thrown from + * {@link javax.sql.DataSource#getConnection()}. + * + * @return the connection timeout in milliseconds + */ + long getConnectionTimeout(); + + /** + * Set the maximum number of milliseconds that a client will wait for a connection from the pool. If this + * time is exceeded without a connection becoming available, a SQLException will be thrown from + * {@link javax.sql.DataSource#getConnection()}. + * + * @param connectionTimeoutMs the connection timeout in milliseconds + */ + void setConnectionTimeout(long connectionTimeoutMs); + + /** + * Get the maximum number of milliseconds that the pool will wait for a connection to be validated as + * alive. + * + * @return the validation timeout in milliseconds + */ + long getValidationTimeout(); + + /** + * Sets the maximum number of milliseconds that the pool will wait for a connection to be validated as + * alive. + * + * @param validationTimeoutMs the validation timeout in milliseconds + */ + void setValidationTimeout(long validationTimeoutMs); + + /** + * This property controls the maximum amount of time (in milliseconds) that a connection is allowed to sit + * idle in the pool. Whether a connection is retired as idle or not is subject to a maximum variation of +30 + * seconds, and average variation of +15 seconds. A connection will never be retired as idle before this timeout. + * A value of 0 means that idle connections are never removed from the pool. + * + * @return the idle timeout in milliseconds + */ + long getIdleTimeout(); + + /** + * This property controls the maximum amount of time (in milliseconds) that a connection is allowed to sit + * idle in the pool. Whether a connection is retired as idle or not is subject to a maximum variation of +30 + * seconds, and average variation of +15 seconds. A connection will never be retired as idle before this timeout. + * A value of 0 means that idle connections are never removed from the pool. + * + * @param idleTimeoutMs the idle timeout in milliseconds + */ + void setIdleTimeout(long idleTimeoutMs); + + /** + * This property controls the amount of time that a connection can be out of the pool before a message is + * logged indicating a possible connection leak. A value of 0 means leak detection is disabled. + * + * @return the connection leak detection threshold in milliseconds + */ + long getLeakDetectionThreshold(); + + /** + * This property controls the amount of time that a connection can be out of the pool before a message is + * logged indicating a possible connection leak. A value of 0 means leak detection is disabled. + * + * @param leakDetectionThresholdMs the connection leak detection threshold in milliseconds + */ + void setLeakDetectionThreshold(long leakDetectionThresholdMs); + + /** + * This property controls the maximum lifetime of a connection in the pool. When a connection reaches this + * timeout, even if recently used, it will be retired from the pool. An in-use connection will never be + * retired, only when it is idle will it be removed. + * + * @return the maximum connection lifetime in milliseconds + */ + long getMaxLifetime(); + + /** + * This property controls the maximum lifetime of a connection in the pool. When a connection reaches this + * timeout, even if recently used, it will be retired from the pool. An in-use connection will never be + * retired, only when it is idle will it be removed. + * + * @param maxLifetimeMs the maximum connection lifetime in milliseconds + */ + void setMaxLifetime(long maxLifetimeMs); + + /** + * The property controls the maximum size that the pool is allowed to reach, including both idle and in-use + * connections. Basically this value will determine the maximum number of actual connections to the database + * backend. + * <p> + * When the pool reaches this size, and no idle connections are available, calls to getConnection() will + * block for up to connectionTimeout milliseconds before timing out. + * + * @return the minimum number of connections in the pool + */ + int getMinimumIdle(); + + /** + * The property controls the minimum number of idle connections that HikariCP tries to maintain in the pool, + * including both idle and in-use connections. If the idle connections dip below this value, HikariCP will + * make a best effort to restore them quickly and efficiently. + * + * @param minIdle the minimum number of idle connections in the pool to maintain + */ + void setMinimumIdle(int minIdle); + + /** + * The property controls the maximum number of connections that HikariCP will keep in the pool, + * including both idle and in-use connections. + * + * @return the maximum number of connections in the pool + */ + int getMaximumPoolSize(); + + /** + * The property controls the maximum size that the pool is allowed to reach, including both idle and in-use + * connections. Basically this value will determine the maximum number of actual connections to the database + * backend. + * <p> + * When the pool reaches this size, and no idle connections are available, calls to getConnection() will + * block for up to connectionTimeout milliseconds before timing out. + * + * @param maxPoolSize the maximum number of connections in the pool + */ + void setMaximumPoolSize(int maxPoolSize); + + /** + * Set the password used for authentication. Changing this at runtime will apply to new connections only. + * Altering this at runtime only works for DataSource-based connections, not Driver-class or JDBC URL-based + * connections. + * + * @param password the database password + */ + void setPassword(String password); + + /** + * Set the username used for authentication. Changing this at runtime will apply to new connections only. + * Altering this at runtime only works for DataSource-based connections, not Driver-class or JDBC URL-based + * connections. + * + * @param username the database username + */ + void setUsername(String username); + + + /** + * The name of the connection pool. + * + * @return the name of the connection pool + */ + String getPoolName(); +}
\ No newline at end of file diff --git a/src/main/java/com/zaxxer/hikari/HikariDataSource.java b/src/main/java/com/zaxxer/hikari/HikariDataSource.java new file mode 100644 index 0000000..29b8953 --- /dev/null +++ b/src/main/java/com/zaxxer/hikari/HikariDataSource.java @@ -0,0 +1,356 @@ +/* + * Copyright (C) 2013 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; + +import java.io.Closeable; +import java.io.PrintWriter; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.sql.DataSource; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.zaxxer.hikari.metrics.MetricsTrackerFactory; +import com.zaxxer.hikari.pool.HikariPool; +import com.zaxxer.hikari.pool.HikariPool.PoolInitializationException; + +/** + * The HikariCP pooled DataSource. + * + * @author Brett Wooldridge + */ +public class HikariDataSource extends HikariConfig implements DataSource, Closeable +{ + private static final Logger LOGGER = LoggerFactory.getLogger(HikariDataSource.class); + + private final AtomicBoolean isShutdown = new AtomicBoolean(); + + private final HikariPool fastPathPool; + private volatile HikariPool pool; + + /** + * Default constructor. Setters be used to configure the pool. Using + * this constructor vs. {@link #HikariDataSource(HikariConfig)} will + * result in {@link #getConnection()} performance that is slightly lower + * due to lazy initialization checks. + */ + public HikariDataSource() + { + super(); + fastPathPool = null; + } + + /** + * Construct a HikariDataSource with the specified configuration. + * + * @param configuration a HikariConfig instance + */ + public HikariDataSource(HikariConfig configuration) + { + configuration.validate(); + configuration.copyState(this); + + LOGGER.info("{} - Starting...", configuration.getPoolName()); + pool = fastPathPool = new HikariPool(this); + LOGGER.info("{} - Start completed.", configuration.getPoolName()); + } + + /** {@inheritDoc} */ + @Override + public Connection getConnection() throws SQLException + { + if (isClosed()) { + throw new SQLException("HikariDataSource " + this + " has been closed."); + } + + if (fastPathPool != null) { + return fastPathPool.getConnection(); + } + + // See http://en.wikipedia.org/wiki/Double-checked_locking#Usage_in_Java + HikariPool result = pool; + if (result == null) { + synchronized (this) { + result = pool; + if (result == null) { + validate(); + LOGGER.info("{} - Starting...", getPoolName()); + try { + pool = result = new HikariPool(this); + } + catch (PoolInitializationException pie) { + if (pie.getCause() instanceof SQLException) { + throw (SQLException) pie.getCause(); + } + else { + throw pie; + } + } + LOGGER.info("{} - Start completed.", getPoolName()); + } + } + } + + return result.getConnection(); + } + + /** {@inheritDoc} */ + @Override + public Connection getConnection(String username, String password) throws SQLException + { + throw new SQLFeatureNotSupportedException(); + } + + /** {@inheritDoc} */ + @Override + public PrintWriter getLogWriter() throws SQLException + { + HikariPool p = pool; + return (p != null ? p.getUnwrappedDataSource().getLogWriter() : null); + } + + /** {@inheritDoc} */ + @Override + public void setLogWriter(PrintWriter out) throws SQLException + { + HikariPool p = pool; + if (p != null) { + p.getUnwrappedDataSource().setLogWriter(out); + } + } + + /** {@inheritDoc} */ + @Override + public void setLoginTimeout(int seconds) throws SQLException + { + HikariPool p = pool; + if (p != null) { + p.getUnwrappedDataSource().setLoginTimeout(seconds); + } + } + + /** {@inheritDoc} */ + @Override + public int getLoginTimeout() throws SQLException + { + HikariPool p = pool; + return (p != null ? p.getUnwrappedDataSource().getLoginTimeout() : 0); + } + + /** {@inheritDoc} */ + @Override + public java.util.logging.Logger getParentLogger() throws SQLFeatureNotSupportedException + { + throw new SQLFeatureNotSupportedException(); + } + + /** {@inheritDoc} */ + @Override + @SuppressWarnings("unchecked") + public <T> T unwrap(Class<T> iface) throws SQLException + { + if (iface.isInstance(this)) { + return (T) this; + } + + HikariPool p = pool; + if (p != null) { + final DataSource unwrappedDataSource = p.getUnwrappedDataSource(); + if (iface.isInstance(unwrappedDataSource)) { + return (T) unwrappedDataSource; + } + + if (unwrappedDataSource != null) { + return unwrappedDataSource.unwrap(iface); + } + } + + throw new SQLException("Wrapped DataSource is not an instance of " + iface); + } + + /** {@inheritDoc} */ + @Override + public boolean isWrapperFor(Class<?> iface) throws SQLException + { + if (iface.isInstance(this)) { + return true; + } + + HikariPool p = pool; + if (p != null) { + final DataSource unwrappedDataSource = p.getUnwrappedDataSource(); + if (iface.isInstance(unwrappedDataSource)) { + return true; + } + + if (unwrappedDataSource != null) { + return unwrappedDataSource.isWrapperFor(iface); + } + } + + return false; + } + + /** {@inheritDoc} */ + @Override + public void setMetricRegistry(Object metricRegistry) + { + boolean isAlreadySet = getMetricRegistry() != null; + super.setMetricRegistry(metricRegistry); + + HikariPool p = pool; + if (p != null) { + if (isAlreadySet) { + throw new IllegalStateException("MetricRegistry can only be set one time"); + } + else { + p.setMetricRegistry(super.getMetricRegistry()); + } + } + } + + /** {@inheritDoc} */ + @Override + public void setMetricsTrackerFactory(MetricsTrackerFactory metricsTrackerFactory) + { + boolean isAlreadySet = getMetricsTrackerFactory() != null; + super.setMetricsTrackerFactory(metricsTrackerFactory); + + HikariPool p = pool; + if (p != null) { + if (isAlreadySet) { + throw new IllegalStateException("MetricsTrackerFactory can only be set one time"); + } + else { + p.setMetricsTrackerFactory(super.getMetricsTrackerFactory()); + } + } + } + + /** {@inheritDoc} */ + @Override + public void setHealthCheckRegistry(Object healthCheckRegistry) + { + boolean isAlreadySet = getHealthCheckRegistry() != null; + super.setHealthCheckRegistry(healthCheckRegistry); + + HikariPool p = pool; + if (p != null) { + if (isAlreadySet) { + throw new IllegalStateException("HealthCheckRegistry can only be set one time"); + } + else { + p.setHealthCheckRegistry(super.getHealthCheckRegistry()); + } + } + } + + /** + * Evict a connection from the pool. If the connection has already been closed (returned to the pool) + * this may result in a "soft" eviction; the connection will be evicted sometime in the future if it is + * currently in use. If the connection has not been closed, the eviction is immediate. + * + * @param connection the connection to evict from the pool + */ + public void evictConnection(Connection connection) + { + HikariPool p; + if (!isClosed() && (p = pool) != null && connection.getClass().getName().startsWith("com.zaxxer.hikari")) { + p.evictConnection(connection); + } + } + + /** + * Suspend allocation of connections from the pool. All callers to <code>getConnection()</code> + * will block indefinitely until <code>resumePool()</code> is called. + */ + public void suspendPool() + { + HikariPool p; + if (!isClosed() && (p = pool) != null) { + p.suspendPool(); + } + } + + /** + * Resume allocation of connections from the pool. + */ + public void resumePool() + { + HikariPool p; + if (!isClosed() && (p = pool) != null) { + p.resumePool(); + } + } + + /** + * Shutdown the DataSource and its associated pool. + */ + @Override + public void close() + { + if (isShutdown.getAndSet(true)) { + return; + } + + HikariPool p = pool; + if (p != null) { + try { + LOGGER.info("{} - Shutdown initiated...", getPoolName()); + p.shutdown(); + LOGGER.info("{} - Shutdown completed.", getPoolName()); + } + catch (InterruptedException e) { + LOGGER.warn("{} - Interrupted during closing", getPoolName(), e); + Thread.currentThread().interrupt(); + } + } + } + + /** + * Determine whether the HikariDataSource has been closed. + * + * @return true if the HikariDataSource has been closed, false otherwise + */ + public boolean isClosed() + { + return isShutdown.get(); + } + + /** + * Shutdown the DataSource and its associated pool. + * + * @deprecated This method has been deprecated, please use {@link #close()} instead + */ + @Deprecated + public void shutdown() + { + LOGGER.warn("The shutdown() method has been deprecated, please use the close() method instead"); + close(); + } + + /** {@inheritDoc} */ + @Override + public String toString() + { + return "HikariDataSource (" + pool + ")"; + } +} diff --git a/src/main/java/com/zaxxer/hikari/HikariJNDIFactory.java b/src/main/java/com/zaxxer/hikari/HikariJNDIFactory.java new file mode 100644 index 0000000..5136028 --- /dev/null +++ b/src/main/java/com/zaxxer/hikari/HikariJNDIFactory.java @@ -0,0 +1,101 @@ +/* + * 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; + +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.Properties; +import java.util.Set; + +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.Name; +import javax.naming.NamingException; +import javax.naming.RefAddr; +import javax.naming.Reference; +import javax.naming.spi.ObjectFactory; +import javax.sql.DataSource; + +import com.zaxxer.hikari.util.PropertyElf; + +/** + * A JNDI factory that produces HikariDataSource instances. + * + * @author Brett Wooldridge + */ +public class HikariJNDIFactory implements ObjectFactory +{ + @Override + synchronized public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception + { + // We only know how to deal with <code>javax.naming.Reference</code> that specify a class name of "javax.sql.DataSource" + if (!(obj instanceof Reference)) { + return null; + } + + Reference ref = (Reference) obj; + if (!"javax.sql.DataSource".equals(ref.getClassName())) { + throw new NamingException(ref.getClassName() + " is not a valid class name/type for this JNDI factory."); + } + + Set<String> hikariPropSet = PropertyElf.getPropertyNames(HikariConfig.class); + + Properties properties = new Properties(); + Enumeration<RefAddr> enumeration = ref.getAll(); + while (enumeration.hasMoreElements()) { + RefAddr element = enumeration.nextElement(); + String type = element.getType(); + if (type.startsWith("dataSource.") || hikariPropSet.contains(type)) { + properties.setProperty(type, element.getContent().toString()); + } + } + + return createDataSource(properties, nameCtx); + } + + private DataSource createDataSource(final Properties properties, final Context context) throws NamingException + { + String jndiName = properties.getProperty("dataSourceJNDI"); + if (jndiName != null) { + return lookupJndiDataSource(properties, context, jndiName); + } + + return new HikariDataSource(new HikariConfig(properties)); + } + + private DataSource lookupJndiDataSource(final Properties properties, final Context context, final String jndiName) throws NamingException + { + if (context == null) { + throw new RuntimeException("JNDI context does not found for dataSourceJNDI : " + jndiName); + } + + DataSource jndiDS = (DataSource) context.lookup(jndiName); + if (jndiDS == null) { + final Context ic = new InitialContext(); + jndiDS = (DataSource) ic.lookup(jndiName); + ic.close(); + } + + if (jndiDS != null) { + HikariConfig config = new HikariConfig(properties); + config.setDataSource(jndiDS); + return new HikariDataSource(config); + } + + return null; + } +} diff --git a/src/main/java/com/zaxxer/hikari/HikariPoolMXBean.java b/src/main/java/com/zaxxer/hikari/HikariPoolMXBean.java new file mode 100644 index 0000000..22c3dbc --- /dev/null +++ b/src/main/java/com/zaxxer/hikari/HikariPoolMXBean.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2013 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; + +/** + * The javax.management MBean for a Hikari pool instance. + * + * @author Brett Wooldridge + */ +public interface HikariPoolMXBean +{ + int getIdleConnections(); + + int getActiveConnections(); + + int getTotalConnections(); + + int getThreadsAwaitingConnection(); + + void softEvictConnections(); + + void suspendPool(); + + void resumePool(); +} diff --git a/src/main/java/com/zaxxer/hikari/hibernate/HikariConfigurationUtil.java b/src/main/java/com/zaxxer/hikari/hibernate/HikariConfigurationUtil.java new file mode 100644 index 0000000..7b58e2d --- /dev/null +++ b/src/main/java/com/zaxxer/hikari/hibernate/HikariConfigurationUtil.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2013 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.hibernate; + +import java.util.Map; +import java.util.Properties; + +import org.hibernate.cfg.AvailableSettings; + +import com.zaxxer.hikari.HikariConfig; + +/** + * Utility class to map Hibernate properties to HikariCP configuration properties. + * + * @author Brett Wooldridge, Luca Burgazzoli + */ +public class HikariConfigurationUtil +{ + public static final String CONFIG_PREFIX = "hibernate.hikari."; + public static final String CONFIG_PREFIX_DATASOURCE = "hibernate.hikari.dataSource."; + + /** + * Create/load a HikariConfig from Hibernate properties. + * + * @param props a map of Hibernate properties + * @return a HikariConfig + */ + @SuppressWarnings("rawtypes") + public static HikariConfig loadConfiguration(Map props) + { + Properties hikariProps = new Properties(); + copyProperty(AvailableSettings.ISOLATION, props, "transactionIsolation", hikariProps); + copyProperty(AvailableSettings.AUTOCOMMIT, props, "autoCommit", hikariProps); + copyProperty(AvailableSettings.DRIVER, props, "driverClassName", hikariProps); + copyProperty(AvailableSettings.URL, props, "jdbcUrl", hikariProps); + copyProperty(AvailableSettings.USER, props, "username", hikariProps); + copyProperty(AvailableSettings.PASS, props, "password", hikariProps); + + for (Object keyo : props.keySet()) { + String key = (String) keyo; + if (key.startsWith(CONFIG_PREFIX)) { + hikariProps.setProperty(key.substring(CONFIG_PREFIX.length()), (String) props.get(key)); + } + } + + return new HikariConfig(hikariProps); + } + + @SuppressWarnings("rawtypes") + private static void copyProperty(String srcKey, Map src, String dstKey, Properties dst) + { + if (src.containsKey(srcKey)) { + dst.setProperty(dstKey, (String) src.get(srcKey)); + } + } +}
\ No newline at end of file diff --git a/src/main/java/com/zaxxer/hikari/hibernate/HikariConnectionProvider.java b/src/main/java/com/zaxxer/hikari/hibernate/HikariConnectionProvider.java new file mode 100644 index 0000000..4f9656c --- /dev/null +++ b/src/main/java/com/zaxxer/hikari/hibernate/HikariConnectionProvider.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2013 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.hibernate; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Map; + +import org.hibernate.HibernateException; +import org.hibernate.Version; +import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider; +import org.hibernate.service.UnknownUnwrapTypeException; +import org.hibernate.service.spi.Configurable; +import org.hibernate.service.spi.Stoppable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; + +import javax.sql.DataSource; + +/** + * Connection provider for Hibernate 4.3. + * + * @author Brett Wooldridge, Luca Burgazzoli + */ +public class HikariConnectionProvider implements ConnectionProvider, Configurable, Stoppable +{ + private static final long serialVersionUID = -9131625057941275711L; + + private static final Logger LOGGER = LoggerFactory.getLogger(HikariConnectionProvider.class); + + /** + * HikariCP configuration. + */ + private HikariConfig hcfg; + + /** + * HikariCP data source. + */ + private HikariDataSource hds; + + // ************************************************************************* + // + // ************************************************************************* + + /** + * c-tor + */ + public HikariConnectionProvider() + { + this.hcfg = null; + this.hds = null; + if (Version.getVersionString().substring(0, 5).compareTo("4.3.6") >= 1) { + LOGGER.warn("com.zaxxer.hikari.hibernate.HikariConnectionProvider has been deprecated for versions of " + + "Hibernate 4.3.6 and newer. Please switch to org.hibernate.hikaricp.internal.HikariCPConnectionProvider."); + } + } + + // ************************************************************************* + // Configurable + // ************************************************************************* + + @SuppressWarnings("rawtypes") + @Override + public void configure(Map props) throws HibernateException + { + try { + LOGGER.debug("Configuring HikariCP"); + + this.hcfg = HikariConfigurationUtil.loadConfiguration(props); + this.hds = new HikariDataSource(this.hcfg); + + } + catch (Exception e) { + throw new HibernateException(e); + } + + LOGGER.debug("HikariCP Configured"); + } + + // ************************************************************************* + // ConnectionProvider + // ************************************************************************* + + @Override + public Connection getConnection() throws SQLException + { + Connection conn = null; + if (this.hds != null) { + conn = this.hds.getConnection(); + } + + return conn; + } + + @Override + public void closeConnection(Connection conn) throws SQLException + { + conn.close(); + } + + @Override + public boolean supportsAggressiveRelease() + { + return false; + } + + @Override + @SuppressWarnings("rawtypes") + public boolean isUnwrappableAs(Class unwrapType) + { + return ConnectionProvider.class.equals(unwrapType) || HikariConnectionProvider.class.isAssignableFrom(unwrapType); + } + + @Override + @SuppressWarnings("unchecked") + public <T> T unwrap(Class<T> unwrapType) + { + if ( ConnectionProvider.class.equals( unwrapType ) || + HikariConnectionProvider.class.isAssignableFrom( unwrapType ) ) { + return (T) this; + } + else if ( DataSource.class.isAssignableFrom( unwrapType ) ) { + return (T) this.hds; + } + else { + throw new UnknownUnwrapTypeException( unwrapType ); + } + } + + // ************************************************************************* + // Stoppable + // ************************************************************************* + + @Override + public void stop() + { + this.hds.close(); + } +} diff --git a/src/main/java/com/zaxxer/hikari/metrics/IMetricsTracker.java b/src/main/java/com/zaxxer/hikari/metrics/IMetricsTracker.java new file mode 100644 index 0000000..18ae9a5 --- /dev/null +++ b/src/main/java/com/zaxxer/hikari/metrics/IMetricsTracker.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2017 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.metrics; + +/** + * @author Brett Wooldridge + */ +public interface IMetricsTracker extends AutoCloseable +{ + default void recordConnectionCreatedMillis(long connectionCreatedMillis) {} + + default void recordConnectionAcquiredNanos(final long elapsedAcquiredNanos) {} + + default void recordConnectionUsageMillis(final long elapsedBorrowedMillis) {} + + default void recordConnectionTimeout() {} + + @Override + default void close() {} +} diff --git a/src/main/java/com/zaxxer/hikari/metrics/MetricsTracker.java b/src/main/java/com/zaxxer/hikari/metrics/MetricsTracker.java new file mode 100755 index 0000000..c294fe6 --- /dev/null +++ b/src/main/java/com/zaxxer/hikari/metrics/MetricsTracker.java @@ -0,0 +1,27 @@ +/* + * 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.metrics; + +/** + * This class only supports realtime, not historical metrics. + * + * @author Brett Wooldridge + */ +@Deprecated +public class MetricsTracker implements IMetricsTracker +{ +} diff --git a/src/main/java/com/zaxxer/hikari/metrics/MetricsTrackerFactory.java b/src/main/java/com/zaxxer/hikari/metrics/MetricsTrackerFactory.java new file mode 100644 index 0000000..3b1c8c1 --- /dev/null +++ b/src/main/java/com/zaxxer/hikari/metrics/MetricsTrackerFactory.java @@ -0,0 +1,29 @@ +/* + * 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.metrics; + +public interface MetricsTrackerFactory +{ + /** + * Create an instance of an IMetricsTracker. + * + * @param poolName the name of the pool + * @param poolStats a PoolStats instance to use + * @return a IMetricsTracker implementation instance + */ + IMetricsTracker create(String poolName, PoolStats poolStats); +} diff --git a/src/main/java/com/zaxxer/hikari/metrics/PoolStats.java b/src/main/java/com/zaxxer/hikari/metrics/PoolStats.java new file mode 100644 index 0000000..e8905d1 --- /dev/null +++ b/src/main/java/com/zaxxer/hikari/metrics/PoolStats.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2015 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.metrics; + +import static com.zaxxer.hikari.util.ClockSource.currentTime; +import static com.zaxxer.hikari.util.ClockSource.plusMillis; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * + * @author Brett Wooldridge + */ +public abstract class PoolStats +{ + private final AtomicLong reloadAt; + private final long timeoutMs; + + protected volatile int totalConnections; + protected volatile int idleConnections; + protected volatile int activeConnections; + protected volatile int pendingThreads; + + public PoolStats(final long timeoutMs) + { + this.timeoutMs = timeoutMs; + this.reloadAt = new AtomicLong(); + } + + public int getTotalConnections() + { + if (shouldLoad()) { + update(); + } + + return totalConnections; + } + + public int getIdleConnections() + { + if (shouldLoad()) { + update(); + } + + return idleConnections; + } + + public int getActiveConnections() + { + if (shouldLoad()) { + update(); + } + + return activeConnections; + } + + public int getPendingThreads() + { + if (shouldLoad()) { + update(); + } + + return pendingThreads; + } + + protected abstract void update(); + + private boolean shouldLoad() + { + for (; ; ) { + final long now = currentTime(); + final long reloadTime = reloadAt.get(); + if (reloadTime > now) { + return false; + } + else if (reloadAt.compareAndSet(reloadTime, plusMillis(now, timeoutMs))) { + return true; + } + } + } +} diff --git a/src/main/java/com/zaxxer/hikari/metrics/dropwizard/CodaHaleMetricsTracker.java b/src/main/java/com/zaxxer/hikari/metrics/dropwizard/CodaHaleMetricsTracker.java new file mode 100755 index 0000000..5be5e23 --- /dev/null +++ b/src/main/java/com/zaxxer/hikari/metrics/dropwizard/CodaHaleMetricsTracker.java @@ -0,0 +1,144 @@ +/* + * 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.metrics.dropwizard; + +import java.util.concurrent.TimeUnit; + +import com.codahale.metrics.Gauge; +import com.codahale.metrics.Histogram; +import com.codahale.metrics.Meter; +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Timer; +import com.zaxxer.hikari.metrics.IMetricsTracker; +import com.zaxxer.hikari.metrics.PoolStats; + +public final class CodaHaleMetricsTracker implements IMetricsTracker +{ + private final String poolName; + private final Timer connectionObtainTimer; + private final Histogram connectionUsage; + private final Histogram connectionCreation; + private final Meter connectionTimeoutMeter; + private final MetricRegistry registry; + + private static final String METRIC_CATEGORY = "pool"; + private static final String METRIC_NAME_WAIT = "Wait"; + private static final String METRIC_NAME_USAGE = "Usage"; + private static final String METRIC_NAME_CONNECT = "ConnectionCreation"; + private static final String METRIC_NAME_TIMEOUT_RATE = "ConnectionTimeoutRate"; + private static final String METRIC_NAME_TOTAL_CONNECTIONS = "TotalConnections"; + private static final String METRIC_NAME_IDLE_CONNECTIONS = "IdleConnections"; + private static final String METRIC_NAME_ACTIVE_CONNECTIONS = "ActiveConnections"; + private static final String METRIC_NAME_PENDING_CONNECTIONS = "PendingConnections"; + + public CodaHaleMetricsTracker(final String poolName, final PoolStats poolStats, final MetricRegistry registry) + { + this.poolName = poolName; + this.registry = registry; + this.connectionObtainTimer = registry.timer(MetricRegistry.name(poolName, METRIC_CATEGORY, METRIC_NAME_WAIT)); + this.connectionUsage = registry.histogram(MetricRegistry.name(poolName, METRIC_CATEGORY, METRIC_NAME_USAGE)); + this.connectionCreation = registry.histogram(MetricRegistry.name(poolName, METRIC_CATEGORY, METRIC_NAME_CONNECT)); + this.connectionTimeoutMeter = registry.meter(MetricRegistry.name(poolName, METRIC_CATEGORY, METRIC_NAME_TIMEOUT_RATE)); + + registry.register(MetricRegistry.name(poolName, METRIC_CATEGORY, METRIC_NAME_TOTAL_CONNECTIONS), + new Gauge<Integer>() { + @Override + public Integer getValue() { + return poolStats.getTotalConnections(); + } + }); + + registry.register(MetricRegistry.name(poolName, METRIC_CATEGORY, METRIC_NAME_IDLE_CONNECTIONS), + new Gauge<Integer>() { + @Override + public Integer getValue() { + return poolStats.getIdleConnections(); + } + }); + + registry.register(MetricRegistry.name(poolName, METRIC_CATEGORY, METRIC_NAME_ACTIVE_CONNECTIONS), + new Gauge<Integer>() { + @Override + public Integer getValue() { + return poolStats.getActiveConnections(); + } + }); + + registry.register(MetricRegistry.name(poolName, METRIC_CATEGORY, METRIC_NAME_PENDING_CONNECTIONS), + new Gauge<Integer>() { + @Override + public Integer getValue() { + return poolStats.getPendingThreads(); + } + }); + } + + /** {@inheritDoc} */ + @Override + public void close() + { + registry.remove(MetricRegistry.name(poolName, METRIC_CATEGORY, METRIC_NAME_WAIT)); + registry.remove(MetricRegistry.name(poolName, METRIC_CATEGORY, METRIC_NAME_USAGE)); + registry.remove(MetricRegistry.name(poolName, METRIC_CATEGORY, METRIC_NAME_CONNECT)); + registry.remove(MetricRegistry.name(poolName, METRIC_CATEGORY, METRIC_NAME_TIMEOUT_RATE)); + registry.remove(MetricRegistry.name(poolName, METRIC_CATEGORY, METRIC_NAME_TOTAL_CONNECTIONS)); + registry.remove(MetricRegistry.name(poolName, METRIC_CATEGORY, METRIC_NAME_IDLE_CONNECTIONS)); + registry.remove(MetricRegistry.name(poolName, METRIC_CATEGORY, METRIC_NAME_ACTIVE_CONNECTIONS)); + registry.remove(MetricRegistry.name(poolName, METRIC_CATEGORY, METRIC_NAME_PENDING_CONNECTIONS)); + } + + /** {@inheritDoc} */ + @Override + public void recordConnectionAcquiredNanos(final long elapsedAcquiredNanos) + { + connectionObtainTimer.update(elapsedAcquiredNanos, TimeUnit.NANOSECONDS); + } + + /** {@inheritDoc} */ + @Override + public void recordConnectionUsageMillis(final long elapsedBorrowedMillis) + { + connectionUsage.update(elapsedBorrowedMillis); + } + + @Override + public void recordConnectionTimeout() + { + connectionTimeoutMeter.mark(); + } + + @Override + public void recordConnectionCreatedMillis(long connectionCreatedMillis) + { + connectionCreation.update(connectionCreatedMillis); + } + + public Timer getConnectionAcquisitionTimer() + { + return connectionObtainTimer; + } + + public Histogram getConnectionDurationHistogram() + { + return connectionUsage; + } + + public Histogram getConnectionCreationHistogram() + { + return connectionCreation; + } +} diff --git a/src/main/java/com/zaxxer/hikari/metrics/dropwizard/CodahaleHealthChecker.java b/src/main/java/com/zaxxer/hikari/metrics/dropwizard/CodahaleHealthChecker.java new file mode 100644 index 0000000..5701dac --- /dev/null +++ b/src/main/java/com/zaxxer/hikari/metrics/dropwizard/CodahaleHealthChecker.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 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.metrics.dropwizard; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Properties; +import java.util.SortedMap; +import java.util.concurrent.TimeUnit; + +import com.codahale.metrics.Metric; +import com.codahale.metrics.MetricFilter; +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Timer; +import com.codahale.metrics.health.HealthCheck; +import com.codahale.metrics.health.HealthCheckRegistry; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.pool.HikariPool; + +/** + * Provides Dropwizard HealthChecks. Two health checks are provided: + * <ul> + * <li>ConnectivityCheck</li> + * <li>Connection99Percent</li> + * </ul> + * The ConnectivityCheck will use the <code>connectionTimeout</code>, unless the health check property + * <code>connectivityCheckTimeoutMs</code> is defined. However, if either the <code>connectionTimeout</code> + * or the <code>connectivityCheckTimeoutMs</code> is 0 (infinite), a timeout of 10 seconds will be used. + * <p> + * The Connection99Percent health check will only be registered if the health check property + * <code>expected99thPercentileMs</code> is defined and greater than 0. + * + * @author Brett Wooldridge + */ +public final class CodahaleHealthChecker +{ + /** + * Register Dropwizard health checks. + * + * @param pool the pool to register health checks for + * @param hikariConfig the pool configuration + * @param registry the HealthCheckRegistry into which checks will be registered + */ + public static void registerHealthChecks(final HikariPool pool, final HikariConfig hikariConfig, final HealthCheckRegistry registry) + { + final Properties healthCheckProperties = hikariConfig.getHealthCheckProperties(); + final MetricRegistry metricRegistry = (MetricRegistry) hikariConfig.getMetricRegistry(); + + final long checkTimeoutMs = Long.parseLong(healthCheckProperties.getProperty("connectivityCheckTimeoutMs", String.valueOf(hikariConfig.getConnectionTimeout()))); + registry.register(MetricRegistry.name(hikariConfig.getPoolName(), "pool", "ConnectivityCheck"), new ConnectivityHealthCheck(pool, checkTimeoutMs)); + + final long expected99thPercentile = Long.parseLong(healthCheckProperties.getProperty("expected99thPercentileMs", "0")); + if (metricRegistry != null && expected99thPercentile > 0) { + SortedMap<String,Timer> timers = metricRegistry.getTimers(new MetricFilter() { + @Override + public boolean matches(String name, Metric metric) + { + return name.equals(MetricRegistry.name(hikariConfig.getPoolName(), "pool", "Wait")); + } + }); + + if (!timers.isEmpty()) { + final Timer timer = timers.entrySet().iterator().next().getValue(); + registry.register(MetricRegistry.name(hikariConfig.getPoolName(), "pool", "Connection99Percent"), new Connection99Percent(timer, expected99thPercentile)); + } + } + } + + private CodahaleHealthChecker() + { + // private constructor + } + + private static class ConnectivityHealthCheck extends HealthCheck + { + private final HikariPool pool; + private final long checkTimeoutMs; + + ConnectivityHealthCheck(final HikariPool pool, final long checkTimeoutMs) + { + this.pool = pool; + this.checkTimeoutMs = (checkTimeoutMs > 0 && checkTimeoutMs != Integer.MAX_VALUE ? checkTimeoutMs : TimeUnit.SECONDS.toMillis(10)); + } + + /** {@inheritDoc} */ + @Override + protected Result check() throws Exception + { + try (Connection connection = pool.getConnection(checkTimeoutMs)) { + return Result.healthy(); + } + catch (SQLException e) { + return Result.unhealthy(e); + } + } + } + + private static class Connection99Percent extends HealthCheck + { + private final Timer waitTimer; + private final long expected99thPercentile; + + Connection99Percent(final Timer waitTimer, final long expected99thPercentile) + { + this.waitTimer = waitTimer; + this.expected99thPercentile = expected99thPercentile; + } + + /** {@inheritDoc} */ + @Override + protected Result check() throws Exception + { + final long the99thPercentile = TimeUnit.NANOSECONDS.toMillis(Math.round(waitTimer.getSnapshot().get99thPercentile())); + return the99thPercentile <= expected99thPercentile ? Result.healthy() : Result.unhealthy("99th percentile connection wait time of %dms exceeds the threshold %dms", the99thPercentile, expected99thPercentile); + } + } +} diff --git a/src/main/java/com/zaxxer/hikari/metrics/dropwizard/CodahaleMetricsTrackerFactory.java b/src/main/java/com/zaxxer/hikari/metrics/dropwizard/CodahaleMetricsTrackerFactory.java new file mode 100644 index 0000000..d978edd --- /dev/null +++ b/src/main/java/com/zaxxer/hikari/metrics/dropwizard/CodahaleMetricsTrackerFactory.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2013 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.metrics.dropwizard; + +import com.codahale.metrics.MetricRegistry; +import com.zaxxer.hikari.metrics.IMetricsTracker; +import com.zaxxer.hikari.metrics.MetricsTrackerFactory; +import com.zaxxer.hikari.metrics.PoolStats; + +public final class CodahaleMetricsTrackerFactory implements MetricsTrackerFactory +{ + private final MetricRegistry registry; + + public CodahaleMetricsTrackerFactory(MetricRegistry registry) + { + this.registry = registry; + } + + public MetricRegistry getRegistry() + { + return registry; + } + + @Override + public IMetricsTracker create(String poolName, PoolStats poolStats) + { + return new CodaHaleMetricsTracker(poolName, poolStats, registry); + } +} diff --git a/src/main/java/com/zaxxer/hikari/metrics/prometheus/HikariCPCollector.java b/src/main/java/com/zaxxer/hikari/metrics/prometheus/HikariCPCollector.java new file mode 100644 index 0000000..3bff974 --- /dev/null +++ b/src/main/java/com/zaxxer/hikari/metrics/prometheus/HikariCPCollector.java @@ -0,0 +1,55 @@ +/* + * 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.metrics.prometheus; + +import com.zaxxer.hikari.metrics.PoolStats; +import io.prometheus.client.Collector; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +class HikariCPCollector extends Collector { + private final PoolStats poolStats; + private final List<String> labelNames; + private final List<String> labelValues; + + HikariCPCollector(String poolName, PoolStats poolStats) { + this.poolStats = poolStats; + this.labelNames = Collections.singletonList("pool"); + this.labelValues = Collections.singletonList(poolName); + } + + @Override + public List<MetricFamilySamples> collect() { + return Arrays.asList( + createSample("hikaricp_active_connections", "Active connections", poolStats.getActiveConnections()), + createSample("hikaricp_idle_connections", "Idle connections", poolStats.getIdleConnections()), + createSample("hikaricp_pending_threads", "Pending threads", poolStats.getPendingThreads()), + createSample("hikaricp_connections", "The number of current connections", poolStats.getTotalConnections()) + ); + } + + private MetricFamilySamples createSample(String name, String helpMessage, double value) + { + List<MetricFamilySamples.Sample> samples = Collections.singletonList( + new MetricFamilySamples.Sample(name, labelNames, labelValues, value) + ); + + return new MetricFamilySamples(name, Type.GAUGE, helpMessage, samples); + } +} diff --git a/src/main/java/com/zaxxer/hikari/metrics/prometheus/PrometheusMetricsTracker.java b/src/main/java/com/zaxxer/hikari/metrics/prometheus/PrometheusMetricsTracker.java new file mode 100644 index 0000000..0bd54fc --- /dev/null +++ b/src/main/java/com/zaxxer/hikari/metrics/prometheus/PrometheusMetricsTracker.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2013 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.metrics.prometheus; + +import com.zaxxer.hikari.metrics.IMetricsTracker; + +import io.prometheus.client.Collector; +import io.prometheus.client.CollectorRegistry; +import io.prometheus.client.Counter; +import io.prometheus.client.Summary; + +class PrometheusMetricsTracker implements IMetricsTracker +{ + private final Counter.Child connectionTimeoutCounter; + private final Summary.Child elapsedAcquiredSummary; + private final Summary.Child elapsedBorrowedSummary; + private final Summary.Child elapsedCreationSummary; + + private final Counter ctCounter; + private final Summary eaSummary; + private final Summary ebSummary; + private final Summary ecSummary; + private final Collector collector; + + PrometheusMetricsTracker(String poolName, Collector collector) + { + this.collector = collector; + + ctCounter = Counter.build() + .name("hikaricp_connection_timeout_count") + .labelNames("pool") + .help("Connection timeout count") + .register(); + + this.connectionTimeoutCounter = ctCounter.labels(poolName); + + eaSummary = Summary.build() + .name("hikaricp_connection_acquired_nanos") + .labelNames("pool") + .help("Connection acquired time (ns)") + .register(); + this.elapsedAcquiredSummary = eaSummary.labels(poolName); + + ebSummary = Summary.build() + .name("hikaricp_connection_usage_millis") + .labelNames("pool") + .help("Connection usage (ms)") + .register(); + this.elapsedBorrowedSummary = ebSummary.labels(poolName); + + ecSummary = Summary.build() + .name("hikaricp_connection_creation_millis") + .labelNames("pool") + .help("Connection creation (ms)") + .register(); + this.elapsedCreationSummary = ecSummary.labels(poolName); + } + + @Override + public void close() + { + CollectorRegistry.defaultRegistry.unregister(ctCounter); + CollectorRegistry.defaultRegistry.unregister(eaSummary); + CollectorRegistry.defaultRegistry.unregister(ebSummary); + CollectorRegistry.defaultRegistry.unregister(ecSummary); + CollectorRegistry.defaultRegistry.unregister(collector); + } + + @Override + public void recordConnectionAcquiredNanos(long elapsedAcquiredNanos) + { + elapsedAcquiredSummary.observe(elapsedAcquiredNanos); + } + + @Override + public void recordConnectionUsageMillis(long elapsedBorrowedMillis) + { + elapsedBorrowedSummary.observe(elapsedBorrowedMillis); + } + + @Override + public void recordConnectionCreatedMillis(long connectionCreatedMillis) + { + elapsedCreationSummary.observe(connectionCreatedMillis); + } + + @Override + public void recordConnectionTimeout() + { + connectionTimeoutCounter.inc(); + } +} diff --git a/src/main/java/com/zaxxer/hikari/metrics/prometheus/PrometheusMetricsTrackerFactory.java b/src/main/java/com/zaxxer/hikari/metrics/prometheus/PrometheusMetricsTrackerFactory.java new file mode 100644 index 0000000..2a0b5c3 --- /dev/null +++ b/src/main/java/com/zaxxer/hikari/metrics/prometheus/PrometheusMetricsTrackerFactory.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2016 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.metrics.prometheus; + +import com.zaxxer.hikari.metrics.IMetricsTracker; +import com.zaxxer.hikari.metrics.MetricsTrackerFactory; +import com.zaxxer.hikari.metrics.PoolStats; + +import io.prometheus.client.Collector; + +/** + * <pre>{@code + * HikariConfig config = new HikariConfig(); + * config.setMetricsTrackerFactory(new PrometheusMetricsTrackerFactory()); + * }</pre> + */ +public class PrometheusMetricsTrackerFactory implements MetricsTrackerFactory +{ + @Override + public IMetricsTracker create(String poolName, PoolStats poolStats) + { + Collector collector = new HikariCPCollector(poolName, poolStats).register(); + return new PrometheusMetricsTracker(poolName, collector); + } +} diff --git a/src/main/java/com/zaxxer/hikari/pool/HikariPool.java b/src/main/java/com/zaxxer/hikari/pool/HikariPool.java new file mode 100755 index 0000000..e9b7364 --- /dev/null +++ b/src/main/java/com/zaxxer/hikari/pool/HikariPool.java @@ -0,0 +1,719 @@ +/* + * 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 java.util.Collections.unmodifiableCollection; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; + +import static com.zaxxer.hikari.pool.PoolEntry.LASTACCESS_REVERSE_COMPARABLE; +import static com.zaxxer.hikari.util.ClockSource.currentTime; +import static com.zaxxer.hikari.util.ClockSource.elapsedDisplayString; +import static com.zaxxer.hikari.util.ClockSource.elapsedMillis; +import static com.zaxxer.hikari.util.ClockSource.plusMillis; +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.createThreadPoolExecutor; +import static com.zaxxer.hikari.util.UtilityElf.quietlySleep; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.SQLTimeoutException; +import java.sql.SQLTransientConnectionException; +import java.util.Collection; +import java.util.Optional; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.ThreadPoolExecutor; + +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.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.util.ConcurrentBag; +import com.zaxxer.hikari.util.ConcurrentBag.IBagStateListener; +import com.zaxxer.hikari.util.SuspendResumeLock; +import com.zaxxer.hikari.util.UtilityElf.DefaultThreadFactory; + +/** + * 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); + + private static final int POOL_NORMAL = 0; + private static final int POOL_SUSPENDED = 1; + private static final int POOL_SHUTDOWN = 2; + + private volatile int poolState; + + private final long ALIVE_BYPASS_WINDOW_MS = Long.getLong("com.zaxxer.hikari.aliveBypassWindowMs", MILLISECONDS.toMillis(500)); + private final long HOUSEKEEPING_PERIOD_MS = Long.getLong("com.zaxxer.hikari.housekeeping.periodMs", SECONDS.toMillis(30)); + + private final PoolEntryCreator POOL_ENTRY_CREATOR = new PoolEntryCreator(null); + private final Collection<Runnable> addConnectionQueue; + private final ThreadPoolExecutor addConnectionExecutor; + private final ThreadPoolExecutor closeConnectionExecutor; + + private final ConcurrentBag<PoolEntry> connectionBag; + + private final ProxyLeakTask leakTask; + private final SuspendResumeLock suspendResumeLock; + + private 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; + + initializeHouseKeepingExecutorService(); + + checkFailFast(); + + if (config.getMetricsTrackerFactory() != null) { + setMetricsTrackerFactory(config.getMetricsTrackerFactory()); + } + else { + setMetricRegistry(config.getMetricRegistry()); + } + + setHealthCheckRegistry(config.getHealthCheckRegistry()); + + registerMBeans(this); + + ThreadFactory threadFactory = config.getThreadFactory(); + + LinkedBlockingQueue<Runnable> addConnectionQueue = new LinkedBlockingQueue<>(config.getMaximumPoolSize()); + this.addConnectionQueue = unmodifiableCollection(addConnectionQueue); + this.addConnectionExecutor = createThreadPoolExecutor(addConnectionQueue, poolName + " connection adder", threadFactory, new ThreadPoolExecutor.DiscardPolicy()); + this.closeConnectionExecutor = createThreadPoolExecutor(config.getMaximumPoolSize(), poolName + " connection closer", threadFactory, new ThreadPoolExecutor.CallerRunsPolicy()); + + this.leakTask = new ProxyLeakTask(config.getLeakDetectionThreshold(), houseKeepingExecutorService); + + this.houseKeeperTask = this.houseKeepingExecutorService.scheduleWithFixedDelay(new HouseKeeper(), 100L, HOUSEKEEPING_PERIOD_MS, MILLISECONDS); + } + + /** + * 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 long startTime = currentTime(); + + try { + long timeout = hardTimeout; + PoolEntry poolEntry = null; + try { + do { + poolEntry = connectionBag.borrow(timeout, MILLISECONDS); + if (poolEntry == null) { + break; // We timed out... break and throw exception + } + + final long now = currentTime(); + if (poolEntry.isMarkedEvicted() || (elapsedMillis(poolEntry.lastAccessed, now) > ALIVE_BYPASS_WINDOW_MS && !isConnectionAlive(poolEntry.connection))) { + closeConnection(poolEntry, "(connection is evicted or dead)"); // Throw away the dead connection (passed max age or failed alive test) + timeout = hardTimeout - elapsedMillis(startTime); + } + else { + metricsTracker.recordBorrowStats(poolEntry, startTime); + return poolEntry.createProxyConnection(leakTask.schedule(poolEntry), now); + } + } while (timeout > 0L); + } + catch (InterruptedException e) { + if (poolEntry != null) { + poolEntry.recycle(startTime); + } + Thread.currentThread().interrupt(); + throw new SQLException(poolName + " - Interrupted during connection acquisition", e); + } + } + finally { + suspendResumeLock.release(); + } + + throw createTimeoutException(startTime); + } + + /** + * 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) { + return; + } + + logPoolState("Before shutdown "); + + if (houseKeeperTask != null) { + houseKeeperTask.cancel(false); + houseKeeperTask = null; + } + + softEvictConnections(); + + addConnectionExecutor.shutdown(); + addConnectionExecutor.awaitTermination(5L, SECONDS); + + destroyHouseKeepingExecutorService(); + + connectionBag.close(); + + final ExecutorService assassinExecutor = createThreadPoolExecutor(config.getMaximumPoolSize(), poolName + " connection assassinator", + config.getThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy()); + try { + final long start = currentTime(); + do { + abortActiveConnections(assassinExecutor); + softEvictConnections(); + } while (getTotalConnections() > 0 && elapsedMillis(start) < SECONDS.toMillis(5)); + } + finally { + assassinExecutor.shutdown(); + assassinExecutor.awaitTermination(5L, SECONDS); + } + + shutdownNetworkTimeoutExecutor(); + closeConnectionExecutor.shutdown(); + closeConnectionExecutor.awaitTermination(5L, SECONDS); + } + finally { + logPoolState("After shutdown "); + unregisterMBeans(); + metricsTracker.close(); + } + } + + /** + * Evict a connection from the pool. + * + * @param connection the connection to evict + */ + public void evictConnection(Connection connection) + { + ProxyConnection 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 + } + } + + public void setMetricRegistry(Object metricRegistry) + { + if (metricRegistry != null) { + setMetricsTrackerFactory(new CodahaleMetricsTrackerFactory((MetricRegistry) metricRegistry)); + } + else { + setMetricsTrackerFactory(null); + } + } + + public void setMetricsTrackerFactory(MetricsTrackerFactory metricsTrackerFactory) + { + if (metricsTrackerFactory != null) { + this.metricsTracker = new MetricsTrackerDelegate(metricsTrackerFactory.create(config.getPoolName(), getPoolStats())); + } + else { + this.metricsTracker = new NopMetricsTrackerDelegate(); + } + } + + public void setHealthCheckRegistry(Object healthCheckRegistry) + { + if (healthCheckRegistry != null) { + CodahaleHealthChecker.registerHealthChecks(this, config, (HealthCheckRegistry) healthCheckRegistry); + } + } + + // *********************************************************************** + // IBagStateListener callback + // *********************************************************************** + + /** {@inheritDoc} */ + @Override + public Future<Boolean> addBagItem(final int waiting) + { + final boolean shouldAdd = waiting - addConnectionQueue.size() >= 0; // Yes, >= is intentional. + if (shouldAdd) { + return addConnectionExecutor.submit(POOL_ENTRY_CREATOR); + } + + return CompletableFuture.completedFuture(Boolean.TRUE); + } + + // *********************************************************************** + // 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(); + 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={}, active={}, idle={}, waiting={})", + poolName, (prefix.length > 0 ? prefix[0] : ""), + getTotalConnections(), getActiveConnections(), getIdleConnections(), getThreadsAwaitingConnection()); + } + } + + /** + * Recycle PoolEntry (add back to the pool) + * + * @param poolEntry the PoolEntry to recycle + */ + @Override + void recycle(final PoolEntry poolEntry) + { + metricsTracker.recordConnectionUsage(poolEntry); + + 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 Connection connection = poolEntry.close(); + closeConnectionExecutor.execute(() -> { + quietlyCloseConnection(connection, closureReason); + if (poolState == POOL_NORMAL) { + fillPool(); + } + }); + } + } + + int[] getPoolStateCounts() + { + return connectionBag.getStateCounts(); + } + + // *********************************************************************** + // Private methods + // *********************************************************************** + + /** + * Creating new poolEntry. + */ + private PoolEntry createPoolEntry() + { + try { + final PoolEntry poolEntry = newPoolEntry(); + + final long maxLifetime = config.getMaxLifetime(); + if (maxLifetime > 0) { + // variance up to 2.5% of the maxlifetime + final long variance = maxLifetime > 10_000 ? ThreadLocalRandom.current().nextLong( maxLifetime / 40 ) : 0; + final long lifetime = maxLifetime - variance; + poolEntry.setFutureEol(houseKeepingExecutorService.schedule( + () -> softEvictConnection(poolEntry, "(connection has passed maxLifetime)", false /* not owner */), + lifetime, MILLISECONDS)); + } + + return poolEntry; + } + catch (Exception e) { + if (poolState == POOL_NORMAL) { + LOGGER.debug("{} - Cannot acquire connection from data source", poolName, (e instanceof ConnectionSetupException ? e.getCause() : 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 int connectionsToAdd = Math.min(config.getMaximumPoolSize() - getTotalConnections(), config.getMinimumIdle() - getIdleConnections()) + - addConnectionQueue.size(); + for (int i = 0; i < connectionsToAdd; i++) { + addConnectionExecutor.submit((i < connectionsToAdd - 1) ? POOL_ENTRY_CREATOR : new PoolEntryCreator("After adding ")); + } + } + + /** + * Attempt to abort or close active connections. + */ + private void abortActiveConnections(final ExecutorService assassinExecutor) + { + for (PoolEntry 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 initializationFailFast is configured, check that we have DB connectivity. + * + * @throws PoolInitializationException if fails to create or validate connection + */ + private void checkFailFast() + { + final long startTime = currentTime(); + Throwable throwable = new SQLTimeoutException("HikariCP was unable to initialize connections in pool " + poolName); + do { + final PoolEntry poolEntry = createPoolEntry(); + if (poolEntry != null) { + if (config.getMinimumIdle() > 0) { + connectionBag.add(poolEntry); + LOGGER.debug("{} - Added connection {}", poolName, poolEntry.connection); + } + else { + final Connection connection = poolEntry.close(); + quietlyCloseConnection(connection, "(initialization check complete and minimumIdle is zero)"); + } + + return; + } + + throwable = getLastConnectionFailure(); + if (throwable instanceof ConnectionSetupException) { + throwPoolInitializationException(throwable.getCause()); + } + + quietlySleep(1000L); + } while (elapsedMillis(startTime) < config.getInitializationFailTimeout()); + + if (config.getInitializationFailTimeout() > 0) { + throwPoolInitializationException(throwable); + } + } + + private void throwPoolInitializationException(Throwable t) + { + LOGGER.error("{} - Exception during pool initialization.", poolName, t); + destroyHouseKeepingExecutorService(); + throw new PoolInitializationException(t); + } + + private void softEvictConnection(final PoolEntry poolEntry, final String reason, final boolean owner) + { + poolEntry.markEvicted(); + if (owner || connectionBag.reserve(poolEntry)) { + closeConnection(poolEntry, reason); + } + } + + private void initializeHouseKeepingExecutorService() + { + if (config.getScheduledExecutor() == null) { + final ThreadFactory threadFactory = Optional.ofNullable(config.getThreadFactory()).orElse(new DefaultThreadFactory(poolName + " housekeeper", true)); + final ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1, threadFactory, new ThreadPoolExecutor.DiscardPolicy()); + executor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false); + executor.setRemoveOnCancelPolicy(true); + this.houseKeepingExecutorService = executor; + } + else { + this.houseKeepingExecutorService = config.getScheduledExecutor(); + } + } + + private void destroyHouseKeepingExecutorService() + { + if (config.getScheduledExecutor() == null) { + houseKeepingExecutorService.shutdownNow(); + } + } + + 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(); + } + }; + } + + private SQLException createTimeoutException(long startTime) + { + logPoolState("Timeout failure "); + metricsTracker.recordConnectionTimeout(); + + String sqlState = null; + final Throwable originalException = getLastConnectionFailure(); + if (originalException instanceof SQLException) { + sqlState = ((SQLException) originalException).getSQLState(); + } + final SQLException connectionException = new SQLTransientConnectionException(poolName + " - Connection is not available, request timed out after " + elapsedMillis(startTime) + "ms.", sqlState, 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<Boolean> + { + private final String afterPrefix; + + PoolEntryCreator(String afterPrefix) + { + this.afterPrefix = afterPrefix; + } + + @Override + public Boolean call() throws Exception + { + long sleepBackoff = 250L; + while (poolState == POOL_NORMAL && shouldCreateAnotherConnection()) { + final PoolEntry poolEntry = createPoolEntry(); + if (poolEntry != null) { + connectionBag.add(poolEntry); + LOGGER.debug("{} - Added connection {}", poolName, poolEntry.connection); + if (afterPrefix != null) { + logPoolState(afterPrefix); + } + return Boolean.TRUE; + } + + // failed to get connection from db, sleep and retry + quietlySleep(sleepBackoff); + sleepBackoff = Math.min(SECONDS.toMillis(10), Math.min(connectionTimeout, (long) (sleepBackoff * 1.5))); + } + // Pool is suspended or shutdown or at max size + return Boolean.FALSE; + } + + private boolean shouldCreateAnotherConnection() { + // only create connections if we need another idle connection or have threads still waiting + // for a new connection, otherwise bail + return getTotalConnections() < config.getMaximumPoolSize() && + (connectionBag.getWaitingThreadCount() > 0 || getIdleConnections() < config.getMinimumIdle()); + } + } + + /** + * The house keeping task to retire and maintain minimum idle connections. + */ + private final class HouseKeeper implements Runnable + { + private volatile long previous = plusMillis(currentTime(), -HOUSEKEEPING_PERIOD_MS); + + @Override + public void run() + { + try { + // refresh timeouts in case they changed via MBean + connectionTimeout = config.getConnectionTimeout(); + validationTimeout = config.getValidationTimeout(); + leakTask.updateLeakDetectionThreshold(config.getLeakDetectionThreshold()); + + final long idleTimeout = config.getIdleTimeout(); + final long now = currentTime(); + + // Detect retrograde time, allowing +128ms as per NTP spec. + if (plusMillis(now, 128) < plusMillis(previous, HOUSEKEEPING_PERIOD_MS)) { + LOGGER.warn("{} - Retrograde clock change detected (housekeeper delta={}), soft-evicting connections from pool.", + poolName, elapsedDisplayString(previous, now)); + previous = now; + softEvictConnections(); + fillPool(); + return; + } + else if (now > plusMillis(previous, (3 * HOUSEKEEPING_PERIOD_MS) / 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; + + String afterPrefix = "Pool "; + if (idleTimeout > 0L && config.getMinimumIdle() < config.getMaximumPoolSize()) { + logPoolState("Before cleanup "); + afterPrefix = "After cleanup "; + + connectionBag + .values(STATE_NOT_IN_USE) + .stream() + .sorted(LASTACCESS_REVERSE_COMPARABLE) + .skip(config.getMinimumIdle()) + .filter(p -> elapsedMillis(p.lastAccessed, now) > idleTimeout) + .filter(p -> connectionBag.reserve(p)) + .forEachOrdered(p -> closeConnection(p, "(connection has passed idleTimeout)")); + } + + logPoolState(afterPrefix); + + fillPool(); // Try to maintain minimum connections + } + catch (Exception e) { + LOGGER.error("Unexpected exception in housekeeping task", e); + } + } + } + + 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); + } + } +} diff --git a/src/main/java/com/zaxxer/hikari/pool/PoolBase.java b/src/main/java/com/zaxxer/hikari/pool/PoolBase.java new file mode 100755 index 0000000..b463dae --- /dev/null +++ b/src/main/java/com/zaxxer/hikari/pool/PoolBase.java @@ -0,0 +1,703 @@ +/* + * 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 java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; + +import java.lang.management.ManagementFactory; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.SQLTransientConnectionException; +import java.sql.Statement; +import java.util.Properties; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.atomic.AtomicReference; + +import javax.management.MBeanServer; +import javax.management.ObjectName; +import javax.sql.DataSource; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.metrics.IMetricsTracker; +import com.zaxxer.hikari.util.DriverDataSource; +import com.zaxxer.hikari.util.PropertyElf; +import com.zaxxer.hikari.util.UtilityElf; +import com.zaxxer.hikari.util.UtilityElf.DefaultThreadFactory; + +import static com.zaxxer.hikari.pool.ProxyConnection.DIRTY_BIT_AUTOCOMMIT; +import static com.zaxxer.hikari.pool.ProxyConnection.DIRTY_BIT_CATALOG; +import static com.zaxxer.hikari.pool.ProxyConnection.DIRTY_BIT_ISOLATION; +import static com.zaxxer.hikari.pool.ProxyConnection.DIRTY_BIT_NETTIMEOUT; +import static com.zaxxer.hikari.pool.ProxyConnection.DIRTY_BIT_READONLY; +import static com.zaxxer.hikari.util.ClockSource.currentTime; +import static com.zaxxer.hikari.util.ClockSource.elapsedMillis; +import static com.zaxxer.hikari.util.ClockSource.elapsedNanos; +import static com.zaxxer.hikari.util.UtilityElf.createInstance; + +abstract class PoolBase +{ + private final Logger LOGGER = LoggerFactory.getLogger(PoolBase.class); + + protected final HikariConfig config; + protected final String poolName; + protected long connectionTimeout; + protected long validationTimeout; + protected IMetricsTrackerDelegate metricsTracker; + + private static final String[] RESET_STATES = {"readOnly", "autoCommit", "isolation", "catalog", "netTimeout"}; + private static final int UNINITIALIZED = -1; + private static final int TRUE = 1; + private static final int FALSE = 0; + + private int networkTimeout; + private int isNetworkTimeoutSupported; + private int isQueryTimeoutSupported; + private int defaultTransactionIsolation; + private int transactionIsolation; + private Executor netTimeoutExecutor; + private DataSource dataSource; + + private final String catalog; + private final boolean isReadOnly; + private final boolean isAutoCommit; + + private final boolean isUseJdbc4Validation; + private final boolean isIsolateInternalQueries; + private final AtomicReference<Throwable> lastConnectionFailure; + + private volatile boolean isValidChecked; + + PoolBase(final HikariConfig config) + { + this.config = config; + + this.networkTimeout = UNINITIALIZED; + this.catalog = config.getCatalog(); + this.isReadOnly = config.isReadOnly(); + this.isAutoCommit = config.isAutoCommit(); + this.transactionIsolation = UtilityElf.getTransactionIsolation(config.getTransactionIsolation()); + + this.isQueryTimeoutSupported = UNINITIALIZED; + this.isNetworkTimeoutSupported = UNINITIALIZED; + this.isUseJdbc4Validation = config.getConnectionTestQuery() == null; + this.isIsolateInternalQueries = config.isIsolateInternalQueries(); + + this.poolName = config.getPoolName(); + this.connectionTimeout = config.getConnectionTimeout(); + this.validationTimeout = config.getValidationTimeout(); + this.lastConnectionFailure = new AtomicReference<>(); + + initializeDataSource(); + } + + /** {@inheritDoc} */ + @Override + public String toString() + { + return poolName; + } + + abstract void recycle(final PoolEntry poolEntry); + + // *********************************************************************** + // JDBC methods + // *********************************************************************** + + void quietlyCloseConnection(final Connection connection, final String closureReason) + { + if (connection != null) { + try { + LOGGER.debug("{} - Closing connection {}: {}", poolName, connection, closureReason); + try { + setNetworkTimeout(connection, SECONDS.toMillis(15)); + } + finally { + connection.close(); // continue with the close even if setNetworkTimeout() throws + } + } + catch (Throwable e) { + LOGGER.debug("{} - Closing connection {} failed", poolName, connection, e); + } + } + } + + boolean isConnectionAlive(final Connection connection) + { + try { + try { + if (isUseJdbc4Validation) { + return connection.isValid((int) MILLISECONDS.toSeconds(Math.max(1000L, validationTimeout))); + } + + setNetworkTimeout(connection, validationTimeout); + + try (Statement statement = connection.createStatement()) { + if (isNetworkTimeoutSupported != TRUE) { + setQueryTimeout(statement, (int) MILLISECONDS.toSeconds(Math.max(1000L, validationTimeout))); + } + + statement.execute(config.getConnectionTestQuery()); + } + } + finally { + if (isIsolateInternalQueries && !isAutoCommit) { + connection.rollback(); + } + } + + setNetworkTimeout(connection, networkTimeout); + + return true; + } + catch (Exception e) { + lastConnectionFailure.set(e); + LOGGER.warn("{} - Failed to validate connection {} ({})", poolName, connection, e.getMessage()); + return false; + } + } + + Throwable getLastConnectionFailure() + { + return lastConnectionFailure.get(); + } + + public DataSource getUnwrappedDataSource() + { + return dataSource; + } + + // *********************************************************************** + // PoolEntry methods + // *********************************************************************** + + PoolEntry newPoolEntry() throws Exception + { + return new PoolEntry(newConnection(), this, isReadOnly, isAutoCommit); + } + + void resetConnectionState(final Connection connection, final ProxyConnection proxyConnection, final int dirtyBits) throws SQLException + { + int resetBits = 0; + + if ((dirtyBits & DIRTY_BIT_READONLY) != 0 && proxyConnection.getReadOnlyState() != isReadOnly) { + connection.setReadOnly(isReadOnly); + resetBits |= DIRTY_BIT_READONLY; + } + + if ((dirtyBits & DIRTY_BIT_AUTOCOMMIT) != 0 && proxyConnection.getAutoCommitState() != isAutoCommit) { + connection.setAutoCommit(isAutoCommit); + resetBits |= DIRTY_BIT_AUTOCOMMIT; + } + + if ((dirtyBits & DIRTY_BIT_ISOLATION) != 0 && proxyConnection.getTransactionIsolationState() != transactionIsolation) { + connection.setTransactionIsolation(transactionIsolation); + resetBits |= DIRTY_BIT_ISOLATION; + } + + if ((dirtyBits & DIRTY_BIT_CATALOG) != 0 && catalog != null && !catalog.equals(proxyConnection.getCatalogState())) { + connection.setCatalog(catalog); + resetBits |= DIRTY_BIT_CATALOG; + } + + if ((dirtyBits & DIRTY_BIT_NETTIMEOUT) != 0 && proxyConnection.getNetworkTimeoutState() != networkTimeout) { + setNetworkTimeout(connection, networkTimeout); + resetBits |= DIRTY_BIT_NETTIMEOUT; + } + + if (resetBits != 0 && LOGGER.isDebugEnabled()) { + LOGGER.debug("{} - Reset ({}) on connection {}", poolName, stringFromResetBits(resetBits), connection); + } + } + + void shutdownNetworkTimeoutExecutor() + { + if (netTimeoutExecutor instanceof ThreadPoolExecutor) { + ((ThreadPoolExecutor) netTimeoutExecutor).shutdownNow(); + } + } + + // *********************************************************************** + // JMX methods + // *********************************************************************** + + /** + * Register MBeans for HikariConfig and HikariPool. + * + * @param pool a HikariPool instance + */ + void registerMBeans(final HikariPool hikariPool) + { + if (!config.isRegisterMbeans()) { + return; + } + + try { + final MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer(); + + final ObjectName beanConfigName = new ObjectName("com.zaxxer.hikari:type=PoolConfig (" + poolName + ")"); + final ObjectName beanPoolName = new ObjectName("com.zaxxer.hikari:type=Pool (" + poolName + ")"); + if (!mBeanServer.isRegistered(beanConfigName)) { + mBeanServer.registerMBean(config, beanConfigName); + mBeanServer.registerMBean(hikariPool, beanPoolName); + } + else { + LOGGER.error("{} - JMX name ({}) is already registered.", poolName, poolName); + } + } + catch (Exception e) { + LOGGER.warn("{} - Failed to register management beans.", poolName, e); + } + } + + /** + * Unregister MBeans for HikariConfig and HikariPool. + */ + void unregisterMBeans() + { + if (!config.isRegisterMbeans()) { + return; + } + + try { + final MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer(); + + final ObjectName beanConfigName = new ObjectName("com.zaxxer.hikari:type=PoolConfig (" + poolName + ")"); + final ObjectName beanPoolName = new ObjectName("com.zaxxer.hikari:type=Pool (" + poolName + ")"); + if (mBeanServer.isRegistered(beanConfigName)) { + mBeanServer.unregisterMBean(beanConfigName); + mBeanServer.unregisterMBean(beanPoolName); + } + } + catch (Exception e) { + LOGGER.warn("{} - Failed to unregister management beans.", poolName, e); + } + } + + // *********************************************************************** + // Private methods + // *********************************************************************** + + /** + * Create/initialize the underlying DataSource. + * + * @return a DataSource instance + */ + private void initializeDataSource() + { + final String jdbcUrl = config.getJdbcUrl(); + final String username = config.getUsername(); + final String password = config.getPassword(); + final String dsClassName = config.getDataSourceClassName(); + final String driverClassName = config.getDriverClassName(); + final Properties dataSourceProperties = config.getDataSourceProperties(); + + DataSource dataSource = config.getDataSource(); + if (dsClassName != null && dataSource == null) { + dataSource = createInstance(dsClassName, DataSource.class); + PropertyElf.setTargetFromProperties(dataSource, dataSourceProperties); + } + else if (jdbcUrl != null && dataSource == null) { + dataSource = new DriverDataSource(jdbcUrl, driverClassName, dataSourceProperties, username, password); + } + + if (dataSource != null) { + setLoginTimeout(dataSource); + createNetworkTimeoutExecutor(dataSource, dsClassName, jdbcUrl); + } + + this.dataSource = dataSource; + } + + /** + * Obtain connection from data source. + * + * @return a Connection connection + */ + Connection newConnection() throws Exception + { + final long start = currentTime(); + + Connection connection = null; + try { + String username = config.getUsername(); + String password = config.getPassword(); + + connection = (username == null) ? dataSource.getConnection() : dataSource.getConnection(username, password); + if (connection == null) { + throw new SQLTransientConnectionException("DataSource returned null unexpectedly"); + } + + setupConnection(connection); + lastConnectionFailure.set(null); + return connection; + } + catch (Exception e) { + if (connection != null) { + quietlyCloseConnection(connection, "(Failed to create/setup connection)"); + } + else if (getLastConnectionFailure() == null) { + LOGGER.debug("{} - Failed to create/setup connection: {}", poolName, e.getMessage()); + } + + lastConnectionFailure.set(e); + throw e; + } + finally { + // tracker will be null during failFast check + if (metricsTracker != null) { + metricsTracker.recordConnectionCreated(elapsedMillis(start)); + } + } + } + + /** + * Setup a connection initial state. + * + * @param connection a Connection + * @throws SQLException thrown from driver + */ + private void setupConnection(final Connection connection) throws ConnectionSetupException + { + try { + if (networkTimeout == UNINITIALIZED) { + networkTimeout = getAndSetNetworkTimeout(connection, validationTimeout); + } + else { + setNetworkTimeout(connection, validationTimeout); + } + + connection.setReadOnly(isReadOnly); + connection.setAutoCommit(isAutoCommit); + + checkDriverSupport(connection); + + if (transactionIsolation != defaultTransactionIsolation) { + connection.setTransactionIsolation(transactionIsolation); + } + + if (catalog != null) { + connection.setCatalog(catalog); + } + + executeSql(connection, config.getConnectionInitSql(), true); + + setNetworkTimeout(connection, networkTimeout); + } + catch (SQLException e) { + throw new ConnectionSetupException(e); + } + } + + /** + * Execute isValid() or connection test query. + * + * @param connection a Connection to check + */ + private void checkDriverSupport(final Connection connection) throws SQLException + { + if (!isValidChecked) { + try { + if (isUseJdbc4Validation) { + connection.isValid(1); + } + else { + executeSql(connection, config.getConnectionTestQuery(), false); + } + } + catch (Throwable e) { + LOGGER.error("{} - Failed to execute" + (isUseJdbc4Validation ? " isValid() for connection, configure" : "") + " connection test query ({}).", poolName, e.getMessage()); + throw e; + } + + try { + defaultTransactionIsolation = connection.getTransactionIsolation(); + if (transactionIsolation == -1) { + transactionIsolation = defaultTransactionIsolation; + } + } + catch (SQLException e) { + LOGGER.warn("{} - Default transaction isolation level detection failed ({}).", poolName, e.getMessage()); + } + finally { + isValidChecked = true; + } + } + } + + /** + * Set the query timeout, if it is supported by the driver. + * + * @param statement a statement to set the query timeout on + * @param timeoutSec the number of seconds before timeout + */ + private void setQueryTimeout(final Statement statement, final int timeoutSec) + { + if (isQueryTimeoutSupported != FALSE) { + try { + statement.setQueryTimeout(timeoutSec); + isQueryTimeoutSupported = TRUE; + } + catch (Throwable e) { + if (isQueryTimeoutSupported == UNINITIALIZED) { + isQueryTimeoutSupported = FALSE; + LOGGER.info("{} - Failed to set query timeout for statement. ({})", poolName, e.getMessage()); + } + } + } + } + + /** + * Set the network timeout, if <code>isUseNetworkTimeout</code> is <code>true</code> and the + * driver supports it. Return the pre-existing value of the network timeout. + * + * @param connection the connection to set the network timeout on + * @param timeoutMs the number of milliseconds before timeout + * @return the pre-existing network timeout value + */ + private int getAndSetNetworkTimeout(final Connection connection, final long timeoutMs) + { + if (isNetworkTimeoutSupported != FALSE) { + try { + final int originalTimeout = connection.getNetworkTimeout(); + connection.setNetworkTimeout(netTimeoutExecutor, (int) timeoutMs); + isNetworkTimeoutSupported = TRUE; + return originalTimeout; + } + catch (Throwable e) { + if (isNetworkTimeoutSupported == UNINITIALIZED) { + isNetworkTimeoutSupported = FALSE; + + LOGGER.info("{} - Driver does not support get/set network timeout for connections. ({})", poolName, e.getMessage()); + if (validationTimeout < SECONDS.toMillis(1)) { + LOGGER.warn("{} - A validationTimeout of less than 1 second cannot be honored on drivers without setNetworkTimeout() support.", poolName); + } + else if (validationTimeout % SECONDS.toMillis(1) != 0) { + LOGGER.warn("{} - A validationTimeout with fractional second granularity cannot be honored on drivers without setNetworkTimeout() support.", poolName); + } + } + } + } + + return 0; + } + + /** + * Set the network timeout, if <code>isUseNetworkTimeout</code> is <code>true</code> and the + * driver supports it. + * + * @param connection the connection to set the network timeout on + * @param timeoutMs the number of milliseconds before timeout + * @throws SQLException throw if the connection.setNetworkTimeout() call throws + */ + private void setNetworkTimeout(final Connection connection, final long timeoutMs) throws SQLException + { + if (isNetworkTimeoutSupported == TRUE) { + connection.setNetworkTimeout(netTimeoutExecutor, (int) timeoutMs); + } + } + + /** + * Execute the user-specified init SQL. + * + * @param connection the connection to initialize + * @param sql the SQL to execute + * @param isCommit whether to commit the SQL after execution or not + * @throws SQLException throws if the init SQL execution fails + */ + private void executeSql(final Connection connection, final String sql, final boolean isCommit) throws SQLException + { + if (sql != null) { + try (Statement statement = connection.createStatement()) { + // connection was created a few milliseconds before, so set query timeout is omitted (we assume it will succeed) + statement.execute(sql); + } + + if (isIsolateInternalQueries && !isAutoCommit) { + if (isCommit) { + connection.commit(); + } + else { + connection.rollback(); + } + } + } + } + + private void createNetworkTimeoutExecutor(final DataSource dataSource, final String dsClassName, final String jdbcUrl) + { + // Temporary hack for MySQL issue: http://bugs.mysql.com/bug.php?id=75615 + if ((dsClassName != null && dsClassName.contains("Mysql")) || + (jdbcUrl != null && jdbcUrl.contains("mysql")) || + (dataSource != null && dataSource.getClass().getName().contains("Mysql"))) { + netTimeoutExecutor = new SynchronousExecutor(); + } + else { + ThreadFactory threadFactory = config.getThreadFactory(); + threadFactory = threadFactory != null ? threadFactory : new DefaultThreadFactory(poolName + " network timeout executor", true); + ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newCachedThreadPool(threadFactory); + executor.setKeepAliveTime(15, SECONDS); + executor.allowCoreThreadTimeOut(true); + netTimeoutExecutor = executor; + } + } + + /** + * Set the loginTimeout on the specified DataSource. + * + * @param dataSource the DataSource + */ + private void setLoginTimeout(final DataSource dataSource) + { + if (connectionTimeout != Integer.MAX_VALUE) { + try { + dataSource.setLoginTimeout(Math.max(1, (int) MILLISECONDS.toSeconds(500L + connectionTimeout))); + } + catch (Throwable e) { + LOGGER.info("{} - Failed to set login timeout for data source. ({})", poolName, e.getMessage()); + } + } + } + + /** + * This will create a string for debug logging. Given a set of "reset bits", this + * method will return a concatenated string, for example: + * + * Input : 0b00110 + * Output: "autoCommit, isolation" + * + * @param bits a set of "reset bits" + * @return a string of which states were reset + */ + private String stringFromResetBits(final int bits) + { + final StringBuilder sb = new StringBuilder(); + for (int ndx = 0; ndx < RESET_STATES.length; ndx++) { + if ( (bits & (0b1 << ndx)) != 0) { + sb.append(RESET_STATES[ndx]).append(", "); + } + } + + sb.setLength(sb.length() - 2); // trim trailing comma + return sb.toString(); + } + + // *********************************************************************** + // Private Static Classes + // *********************************************************************** + + static class ConnectionSetupException extends Exception + { + private static final long serialVersionUID = 929872118275916521L; + + public ConnectionSetupException(Throwable t) + { + super(t); + } + } + + /** + * Special executor used only to work around a MySQL issue that has not been addressed. + * MySQL issue: http://bugs.mysql.com/bug.php?id=75615 + */ + private static class SynchronousExecutor implements Executor + { + /** {@inheritDoc} */ + @Override + public void execute(Runnable command) + { + try { + command.run(); + } + catch (Throwable t) { + LoggerFactory.getLogger(PoolBase.class).debug("Failed to execute: {}", command, t); + } + } + } + + static interface IMetricsTrackerDelegate extends AutoCloseable + { + default void recordConnectionUsage(PoolEntry poolEntry) {} + + default void recordConnectionCreated(long connectionCreatedMillis) {} + + default void recordBorrowStats(final PoolEntry poolEntry, final long startTime) {} + + default void recordConnectionTimeout() {} + + @Override + default void close() {} + } + + /** + * A class that delegates to a MetricsTracker implementation. The use of a delegate + * allows us to use the NopMetricsTrackerDelegate when metrics are disabled, which in + * turn allows the JIT to completely optimize away to callsites to record metrics. + */ + static class MetricsTrackerDelegate implements IMetricsTrackerDelegate + { + final IMetricsTracker tracker; + + MetricsTrackerDelegate(IMetricsTracker tracker) + { + this.tracker = tracker; + } + + @Override + public void recordConnectionUsage(final PoolEntry poolEntry) + { + tracker.recordConnectionUsageMillis(poolEntry.getMillisSinceBorrowed()); + } + + @Override + public void recordConnectionCreated(long connectionCreatedMillis) + { + tracker.recordConnectionCreatedMillis(connectionCreatedMillis); + } + + @Override + public void recordBorrowStats(final PoolEntry poolEntry, final long startTime) + { + final long now = currentTime(); + poolEntry.lastBorrowed = now; + tracker.recordConnectionAcquiredNanos(elapsedNanos(startTime, now)); + } + + @Override + public void recordConnectionTimeout() { + tracker.recordConnectionTimeout(); + } + + @Override + public void close() + { + tracker.close(); + } + } + + /** + * A no-op implementation of the IMetricsTrackerDelegate that is used when metrics capture is + * disabled. + */ + static final class NopMetricsTrackerDelegate implements IMetricsTrackerDelegate {} +} diff --git a/src/main/java/com/zaxxer/hikari/pool/PoolEntry.java b/src/main/java/com/zaxxer/hikari/pool/PoolEntry.java new file mode 100644 index 0000000..5b16047 --- /dev/null +++ b/src/main/java/com/zaxxer/hikari/pool/PoolEntry.java @@ -0,0 +1,203 @@ +/* + * Copyright (C) 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.ClockSource.currentTime; +import static com.zaxxer.hikari.util.ClockSource.elapsedDisplayString; +import static com.zaxxer.hikari.util.ClockSource.elapsedMillis; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Comparator; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.zaxxer.hikari.util.ConcurrentBag.IConcurrentBagEntry; +import com.zaxxer.hikari.util.FastList; + +/** + * Entry used in the ConcurrentBag to track Connection instances. + * + * @author Brett Wooldridge + */ +final class PoolEntry implements IConcurrentBagEntry +{ + private static final Logger LOGGER = LoggerFactory.getLogger(PoolEntry.class); + private static final AtomicIntegerFieldUpdater<PoolEntry> stateUpdater; + + static final Comparator<PoolEntry> LASTACCESS_REVERSE_COMPARABLE; + + Connection connection; + long lastAccessed; + long lastBorrowed; + private volatile int state; + private volatile boolean evict; + + private volatile ScheduledFuture<?> endOfLife; + + private final FastList<Statement> openStatements; + private final HikariPool hikariPool; + + private final boolean isReadOnly; + private final boolean isAutoCommit; + + static + { + LASTACCESS_REVERSE_COMPARABLE = new Comparator<PoolEntry>() { + @Override + public int compare(final PoolEntry entryOne, final PoolEntry entryTwo) { + return Long.compare(entryTwo.lastAccessed, entryOne.lastAccessed); + } + }; + + stateUpdater = AtomicIntegerFieldUpdater.newUpdater(PoolEntry.class, "state"); + } + + PoolEntry(final Connection connection, final PoolBase pool, final boolean isReadOnly, final boolean isAutoCommit) + { + this.connection = connection; + this.hikariPool = (HikariPool) pool; + this.isReadOnly = isReadOnly; + this.isAutoCommit = isAutoCommit; + this.lastAccessed = currentTime(); + this.openStatements = new FastList<>(Statement.class, 16); + } + + /** + * Release this entry back to the pool. + * + * @param lastAccessed last access time-stamp + */ + void recycle(final long lastAccessed) + { + if (connection != null) { + this.lastAccessed = lastAccessed; + hikariPool.recycle(this); + } + } + + /** + * @param endOfLife + */ + void setFutureEol(final ScheduledFuture<?> endOfLife) + { + this.endOfLife = endOfLife; + } + + Connection createProxyConnection(final ProxyLeakTask leakTask, final long now) + { + return ProxyFactory.getProxyConnection(this, connection, openStatements, leakTask, now, isReadOnly, isAutoCommit); + } + + void resetConnectionState(final ProxyConnection proxyConnection, final int dirtyBits) throws SQLException + { + hikariPool.resetConnectionState(connection, proxyConnection, dirtyBits); + } + + String getPoolName() + { + return hikariPool.toString(); + } + + boolean isMarkedEvicted() + { + return evict; + } + + void markEvicted() + { + this.evict = true; + } + + void evict(final String closureReason) + { + hikariPool.closeConnection(this, closureReason); + } + + /** Returns millis since lastBorrowed */ + long getMillisSinceBorrowed() + { + return elapsedMillis(lastBorrowed); + } + + /** {@inheritDoc} */ + @Override + public String toString() + { + final long now = currentTime(); + return connection + + ", accessed " + elapsedDisplayString(lastAccessed, now) + " ago, " + + stateToString(); + } + + // *********************************************************************** + // IConcurrentBagEntry methods + // *********************************************************************** + + /** {@inheritDoc} */ + @Override + public int getState() + { + return stateUpdater.get(this); + } + + /** {@inheritDoc} */ + @Override + public boolean compareAndSet(int expect, int update) + { + return stateUpdater.compareAndSet(this, expect, update); + } + + /** {@inheritDoc} */ + @Override + public void setState(int update) + { + stateUpdater.set(this, update); + } + + Connection close() + { + ScheduledFuture<?> eol = endOfLife; + if (eol != null && !eol.isDone() && !eol.cancel(false)) { + LOGGER.warn("{} - maxLifeTime expiration task cancellation unexpectedly returned false for connection {}", getPoolName(), connection); + } + + Connection con = connection; + connection = null; + endOfLife = null; + return con; + } + + private String stateToString() + { + switch (state) { + case STATE_IN_USE: + return "IN_USE"; + case STATE_NOT_IN_USE: + return "NOT_IN_USE"; + case STATE_REMOVED: + return "REMOVED"; + case STATE_RESERVED: + return "RESERVED"; + default: + return "Invalid"; + } + } +} diff --git a/src/main/java/com/zaxxer/hikari/pool/ProxyCallableStatement.java b/src/main/java/com/zaxxer/hikari/pool/ProxyCallableStatement.java new file mode 100644 index 0000000..922be53 --- /dev/null +++ b/src/main/java/com/zaxxer/hikari/pool/ProxyCallableStatement.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2013 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 java.sql.CallableStatement; + +/** + * This is the proxy class for java.sql.CallableStatement. + * + * @author Brett Wooldridge + */ +public abstract class ProxyCallableStatement extends ProxyPreparedStatement implements CallableStatement +{ + protected ProxyCallableStatement(ProxyConnection connection, CallableStatement statement) + { + super(connection, statement); + } + + // ********************************************************************** + // Overridden java.sql.CallableStatement Methods + // ********************************************************************** + +}
\ No newline at end of file diff --git a/src/main/java/com/zaxxer/hikari/pool/ProxyConnection.java b/src/main/java/com/zaxxer/hikari/pool/ProxyConnection.java new file mode 100644 index 0000000..7143bae --- /dev/null +++ b/src/main/java/com/zaxxer/hikari/pool/ProxyConnection.java @@ -0,0 +1,475 @@ +/* + * 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.ClockSource.currentTime; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.sql.CallableStatement; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Savepoint; +import java.sql.Statement; +import java.sql.Wrapper; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.Executor; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.zaxxer.hikari.util.FastList; + +/** + * This is the proxy class for java.sql.Connection. + * + * @author Brett Wooldridge + */ +public abstract class ProxyConnection implements Connection +{ + static final int DIRTY_BIT_READONLY = 0b00001; + static final int DIRTY_BIT_AUTOCOMMIT = 0b00010; + static final int DIRTY_BIT_ISOLATION = 0b00100; + static final int DIRTY_BIT_CATALOG = 0b01000; + static final int DIRTY_BIT_NETTIMEOUT = 0b10000; + + private static final Logger LOGGER; + private static final Set<String> ERROR_STATES; + private static final Set<Integer> ERROR_CODES; + + protected Connection delegate; + + private final PoolEntry poolEntry; + private final ProxyLeakTask leakTask; + private final FastList<Statement> openStatements; + + private int dirtyBits; + private long lastAccess; + private boolean isCommitStateDirty; + + private boolean isReadOnly; + private boolean isAutoCommit; + private int networkTimeout; + private int transactionIsolation; + private String dbcatalog; + + // static initializer + static { + LOGGER = LoggerFactory.getLogger(ProxyConnection.class); + + ERROR_STATES = new HashSet<>(); + ERROR_STATES.add("57P01"); // ADMIN SHUTDOWN + ERROR_STATES.add("57P02"); // CRASH SHUTDOWN + ERROR_STATES.add("57P03"); // CANNOT CONNECT NOW + ERROR_STATES.add("01002"); // SQL92 disconnect error + ERROR_STATES.add("JZ0C0"); // Sybase disconnect error + ERROR_STATES.add("JZ0C1"); // Sybase disconnect error + + ERROR_CODES = new HashSet<>(); + ERROR_CODES.add(500150); + ERROR_CODES.add(2399); + } + + protected ProxyConnection(final PoolEntry poolEntry, final Connection connection, final FastList<Statement> openStatements, final ProxyLeakTask leakTask, final long now, final boolean isReadOnly, final boolean isAutoCommit) { + this.poolEntry = poolEntry; + this.delegate = connection; + this.openStatements = openStatements; + this.leakTask = leakTask; + this.lastAccess = now; + this.isReadOnly = isReadOnly; + this.isAutoCommit = isAutoCommit; + } + + /** {@inheritDoc} */ + @Override + public final String toString() + { + return new StringBuilder(64) + .append(this.getClass().getSimpleName()).append('@').append(System.identityHashCode(this)) + .append(" wrapping ") + .append(delegate).toString(); + } + + // *********************************************************************** + // Connection State Accessors + // *********************************************************************** + + final boolean getAutoCommitState() + { + return isAutoCommit; + } + + final String getCatalogState() + { + return dbcatalog; + } + + final int getTransactionIsolationState() + { + return transactionIsolation; + } + + final boolean getReadOnlyState() + { + return isReadOnly; + } + + final int getNetworkTimeoutState() + { + return networkTimeout; + } + + // *********************************************************************** + // Internal methods + // *********************************************************************** + + final PoolEntry getPoolEntry() + { + return poolEntry; + } + + final SQLException checkException(SQLException sqle) + { + SQLException nse = sqle; + for (int depth = 0; delegate != ClosedConnection.CLOSED_CONNECTION && nse != null && depth < 10; depth++) { + final String sqlState = nse.getSQLState(); + if (sqlState != null && sqlState.startsWith("08") || ERROR_STATES.contains(sqlState) || ERROR_CODES.contains(nse.getErrorCode())) { + // broken connection + LOGGER.warn("{} - Connection {} marked as broken because of SQLSTATE({}), ErrorCode({})", + poolEntry.getPoolName(), delegate, sqlState, nse.getErrorCode(), nse); + leakTask.cancel(); + poolEntry.evict("(connection is broken)"); + delegate = ClosedConnection.CLOSED_CONNECTION; + } + else { + nse = nse.getNextException(); + } + } + + return sqle; + } + + final synchronized void untrackStatement(final Statement statement) + { + openStatements.remove(statement); + } + + final void markCommitStateDirty() + { + if (isAutoCommit) { + lastAccess = currentTime(); + } + else { + isCommitStateDirty = true; + } + } + + void cancelLeakTask() + { + leakTask.cancel(); + } + + private final synchronized <T extends Statement> T trackStatement(final T statement) + { + openStatements.add(statement); + + return statement; + } + + private final void closeStatements() + { + final int size = openStatements.size(); + if (size > 0) { + for (int i = 0; i < size && delegate != ClosedConnection.CLOSED_CONNECTION; i++) { + try (Statement statement = openStatements.get(i)) { + // automatic resource cleanup + } + catch (SQLException e) { + checkException(e); + } + } + + synchronized (this) { + openStatements.clear(); + } + } + } + + // ********************************************************************** + // "Overridden" java.sql.Connection Methods + // ********************************************************************** + + /** {@inheritDoc} */ + @Override + public final void close() throws SQLException + { + // Closing statements can cause connection eviction, so this must run before the conditional below + closeStatements(); + + if (delegate != ClosedConnection.CLOSED_CONNECTION) { + leakTask.cancel(); + + try { + if (isCommitStateDirty && !isAutoCommit) { + delegate.rollback(); + lastAccess = currentTime(); + LOGGER.debug("{} - Executed rollback on connection {} due to dirty commit state on close().", poolEntry.getPoolName(), delegate); + } + + if (dirtyBits != 0) { + poolEntry.resetConnectionState(this, dirtyBits); + lastAccess = currentTime(); + } + + delegate.clearWarnings(); + } + catch (SQLException e) { + // when connections are aborted, exceptions are often thrown that should not reach the application + if (!poolEntry.isMarkedEvicted()) { + throw checkException(e); + } + } + finally { + delegate = ClosedConnection.CLOSED_CONNECTION; + poolEntry.recycle(lastAccess); + } + } + } + + /** {@inheritDoc} */ + @Override + public boolean isClosed() throws SQLException + { + return (delegate == ClosedConnection.CLOSED_CONNECTION); + } + + /** {@inheritDoc} */ + @Override + public Statement createStatement() throws SQLException + { + return ProxyFactory.getProxyStatement(this, trackStatement(delegate.createStatement())); + } + + /** {@inheritDoc} */ + @Override + public Statement createStatement(int resultSetType, int concurrency) throws SQLException + { + return ProxyFactory.getProxyStatement(this, trackStatement(delegate.createStatement(resultSetType, concurrency))); + } + + /** {@inheritDoc} */ + @Override + public Statement createStatement(int resultSetType, int concurrency, int holdability) throws SQLException + { + return ProxyFactory.getProxyStatement(this, trackStatement(delegate.createStatement(resultSetType, concurrency, holdability))); + } + + /** {@inheritDoc} */ + @Override + public CallableStatement prepareCall(String sql) throws SQLException + { + return ProxyFactory.getProxyCallableStatement(this, trackStatement(delegate.prepareCall(sql))); + } + + /** {@inheritDoc} */ + @Override + public CallableStatement prepareCall(String sql, int resultSetType, int concurrency) throws SQLException + { + return ProxyFactory.getProxyCallableStatement(this, trackStatement(delegate.prepareCall(sql, resultSetType, concurrency))); + } + + /** {@inheritDoc} */ + @Override + public CallableStatement prepareCall(String sql, int resultSetType, int concurrency, int holdability) throws SQLException + { + return ProxyFactory.getProxyCallableStatement(this, trackStatement(delegate.prepareCall(sql, resultSetType, concurrency, holdability))); + } + + /** {@inheritDoc} */ + @Override + public PreparedStatement prepareStatement(String sql) throws SQLException + { + return ProxyFactory.getProxyPreparedStatement(this, trackStatement(delegate.prepareStatement(sql))); + } + + /** {@inheritDoc} */ + @Override + public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException + { + return ProxyFactory.getProxyPreparedStatement(this, trackStatement(delegate.prepareStatement(sql, autoGeneratedKeys))); + } + + /** {@inheritDoc} */ + @Override + public PreparedStatement prepareStatement(String sql, int resultSetType, int concurrency) throws SQLException + { + return ProxyFactory.getProxyPreparedStatement(this, trackStatement(delegate.prepareStatement(sql, resultSetType, concurrency))); + } + + /** {@inheritDoc} */ + @Override + public PreparedStatement prepareStatement(String sql, int resultSetType, int concurrency, int holdability) throws SQLException + { + return ProxyFactory.getProxyPreparedStatement(this, trackStatement(delegate.prepareStatement(sql, resultSetType, concurrency, holdability))); + } + + /** {@inheritDoc} */ + @Override + public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException + { + return ProxyFactory.getProxyPreparedStatement(this, trackStatement(delegate.prepareStatement(sql, columnIndexes))); + } + + /** {@inheritDoc} */ + @Override + public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException + { + return ProxyFactory.getProxyPreparedStatement(this, trackStatement(delegate.prepareStatement(sql, columnNames))); + } + + /** {@inheritDoc} */ + @Override + public void commit() throws SQLException + { + delegate.commit(); + isCommitStateDirty = false; + lastAccess = currentTime(); + } + + /** {@inheritDoc} */ + @Override + public void rollback() throws SQLException + { + delegate.rollback(); + isCommitStateDirty = false; + lastAccess = currentTime(); + } + + /** {@inheritDoc} */ + @Override + public void rollback(Savepoint savepoint) throws SQLException + { + delegate.rollback(savepoint); + isCommitStateDirty = false; + lastAccess = currentTime(); + } + + /** {@inheritDoc} */ + @Override + public void setAutoCommit(boolean autoCommit) throws SQLException + { + delegate.setAutoCommit(autoCommit); + isAutoCommit = autoCommit; + dirtyBits |= DIRTY_BIT_AUTOCOMMIT; + } + + /** {@inheritDoc} */ + @Override + public void setReadOnly(boolean readOnly) throws SQLException + { + delegate.setReadOnly(readOnly); + isReadOnly = readOnly; + isCommitStateDirty = false; + dirtyBits |= DIRTY_BIT_READONLY; + } + + /** {@inheritDoc} */ + @Override + public void setTransactionIsolation(int level) throws SQLException + { + delegate.setTransactionIsolation(level); + transactionIsolation = level; + dirtyBits |= DIRTY_BIT_ISOLATION; + } + + /** {@inheritDoc} */ + @Override + public void setCatalog(String catalog) throws SQLException + { + delegate.setCatalog(catalog); + dbcatalog = catalog; + dirtyBits |= DIRTY_BIT_CATALOG; + } + + /** {@inheritDoc} */ + @Override + public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException + { + delegate.setNetworkTimeout(executor, milliseconds); + networkTimeout = milliseconds; + dirtyBits |= DIRTY_BIT_NETTIMEOUT; + } + + /** {@inheritDoc} */ + @Override + public final boolean isWrapperFor(Class<?> iface) throws SQLException + { + return iface.isInstance(delegate) || (delegate instanceof Wrapper && delegate.isWrapperFor(iface)); + } + + /** {@inheritDoc} */ + @Override + @SuppressWarnings("unchecked") + public final <T> T unwrap(Class<T> iface) throws SQLException + { + if (iface.isInstance(delegate)) { + return (T) delegate; + } + else if (delegate instanceof Wrapper) { + return delegate.unwrap(iface); + } + + throw new SQLException("Wrapped connection is not an instance of " + iface); + } + + // ********************************************************************** + // Private classes + // ********************************************************************** + + private static final class ClosedConnection + { + static final Connection CLOSED_CONNECTION = getClosedConnection(); + + private static Connection getClosedConnection() + { + InvocationHandler handler = new InvocationHandler() { + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable + { + final String methodName = method.getName(); + if ("abort".equals(methodName)) { + return Void.TYPE; + } + else if ("isValid".equals(methodName)) { + return Boolean.FALSE; + } + else if ("toString".equals(methodName)) { + return ClosedConnection.class.getCanonicalName(); + } + + throw new SQLException("Connection is closed"); + } + }; + + return (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), new Class[] { Connection.class }, handler); + } + } +} diff --git a/src/main/java/com/zaxxer/hikari/pool/ProxyFactory.java b/src/main/java/com/zaxxer/hikari/pool/ProxyFactory.java new file mode 100644 index 0000000..026debb --- /dev/null +++ b/src/main/java/com/zaxxer/hikari/pool/ProxyFactory.java @@ -0,0 +1,80 @@ +/* + * 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 java.sql.CallableStatement; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.Statement; + +import com.zaxxer.hikari.util.FastList; + +/** + * A factory class that produces proxies around instances of the standard + * JDBC interfaces. + * + * @author Brett Wooldridge + */ +public final class ProxyFactory +{ + private ProxyFactory() + { + // unconstructable + } + + /** + * Create a proxy for the specified {@link Connection} instance. + * @param poolEntry + * @param connection + * @param openStatements + * @param leakTask + * @param now + * @param isReadOnly + * @param isAutoCommit + * @return a proxy that wraps the specified {@link Connection} + */ + static ProxyConnection getProxyConnection(final PoolEntry poolEntry, final Connection connection, final FastList<Statement> openStatements, final ProxyLeakTask leakTask, final long now, final boolean isReadOnly, final boolean isAutoCommit) + { + // Body is replaced (injected) by JavassistProxyFactory + throw new IllegalStateException("You need to run the CLI build and you need target/classes in your classpath to run."); + } + + static Statement getProxyStatement(final ProxyConnection connection, final Statement statement) + { + // Body is replaced (injected) by JavassistProxyFactory + throw new IllegalStateException("You need to run the CLI build and you need target/classes in your classpath to run."); + } + + static CallableStatement getProxyCallableStatement(final ProxyConnection connection, final CallableStatement statement) + { + // Body is replaced (injected) by JavassistProxyFactory + throw new IllegalStateException("You need to run the CLI build and you need target/classes in your classpath to run."); + } + + static PreparedStatement getProxyPreparedStatement(final ProxyConnection connection, final PreparedStatement statement) + { + // Body is replaced (injected) by JavassistProxyFactory + throw new IllegalStateException("You need to run the CLI build and you need target/classes in your classpath to run."); + } + + static ResultSet getProxyResultSet(final ProxyConnection connection, final ProxyStatement statement, final ResultSet resultSet) + { + // Body is replaced (injected) by JavassistProxyFactory + throw new IllegalStateException("You need to run the CLI build and you need target/classes in your classpath to run."); + } +} diff --git a/src/main/java/com/zaxxer/hikari/pool/ProxyLeakTask.java b/src/main/java/com/zaxxer/hikari/pool/ProxyLeakTask.java new file mode 100644 index 0000000..0fdc93e --- /dev/null +++ b/src/main/java/com/zaxxer/hikari/pool/ProxyLeakTask.java @@ -0,0 +1,100 @@ +/* + * 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 java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A Runnable that is scheduled in the future to report leaks. The ScheduledFuture is + * cancelled if the connection is closed before the leak time expires. + * + * @author Brett Wooldridge + */ +class ProxyLeakTask implements Runnable +{ + private static final Logger LOGGER = LoggerFactory.getLogger(ProxyLeakTask.class); + private static final ProxyLeakTask NO_LEAK; + + private ScheduledExecutorService executorService; + private long leakDetectionThreshold; + private ScheduledFuture<?> scheduledFuture; + private String connectionName; + private Exception exception; + private boolean isLeaked; + + static + { + NO_LEAK = new ProxyLeakTask() { + @Override + public void cancel() {} + }; + } + + ProxyLeakTask(final long leakDetectionThreshold, final ScheduledExecutorService executorService) + { + this.executorService = executorService; + this.leakDetectionThreshold = leakDetectionThreshold; + } + + private ProxyLeakTask(final ProxyLeakTask parent, final PoolEntry poolEntry) + { + this.exception = new Exception("Apparent connection leak detected"); + this.connectionName = poolEntry.connection.toString(); + scheduledFuture = parent.executorService.schedule(this, parent.leakDetectionThreshold, TimeUnit.MILLISECONDS); + } + + private ProxyLeakTask() + { + } + + ProxyLeakTask schedule(final PoolEntry bagEntry) + { + return (leakDetectionThreshold == 0) ? NO_LEAK : new ProxyLeakTask(this, bagEntry); + } + + void updateLeakDetectionThreshold(final long leakDetectionThreshold) + { + this.leakDetectionThreshold = leakDetectionThreshold; + } + + /** {@inheritDoc} */ + @Override + public void run() + { + isLeaked = true; + + final StackTraceElement[] stackTrace = exception.getStackTrace(); + final StackTraceElement[] trace = new StackTraceElement[stackTrace.length - 5]; + System.arraycopy(stackTrace, 5, trace, 0, trace.length); + + exception.setStackTrace(trace); + LOGGER.warn("Connection leak detection triggered for {}, stack trace follows", connectionName, exception); + } + + void cancel() + { + scheduledFuture.cancel(false); + if (isLeaked) { + LOGGER.info("Previously reported leaked connection {} was returned to the pool (unleaked)", connectionName); + } + } +} diff --git a/src/main/java/com/zaxxer/hikari/pool/ProxyPreparedStatement.java b/src/main/java/com/zaxxer/hikari/pool/ProxyPreparedStatement.java new file mode 100644 index 0000000..e2d96c9 --- /dev/null +++ b/src/main/java/com/zaxxer/hikari/pool/ProxyPreparedStatement.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2013 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 java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * This is the proxy class for java.sql.PreparedStatement. + * + * @author Brett Wooldridge + */ +public abstract class ProxyPreparedStatement extends ProxyStatement implements PreparedStatement +{ + protected ProxyPreparedStatement(ProxyConnection connection, PreparedStatement statement) + { + super(connection, statement); + } + + // ********************************************************************** + // Overridden java.sql.PreparedStatement Methods + // ********************************************************************** + + /** {@inheritDoc} */ + @Override + public boolean execute() throws SQLException + { + connection.markCommitStateDirty(); + return ((PreparedStatement) delegate).execute(); + } + + /** {@inheritDoc} */ + @Override + public ResultSet executeQuery() throws SQLException + { + connection.markCommitStateDirty(); + ResultSet resultSet = ((PreparedStatement) delegate).executeQuery(); + return ProxyFactory.getProxyResultSet(connection, this, resultSet); + } + + /** {@inheritDoc} */ + @Override + public int executeUpdate() throws SQLException + { + connection.markCommitStateDirty(); + return ((PreparedStatement) delegate).executeUpdate(); + } + + /** {@inheritDoc} */ + @Override + public long executeLargeUpdate() throws SQLException + { + connection.markCommitStateDirty(); + return ((PreparedStatement) delegate).executeLargeUpdate(); + } +} diff --git a/src/main/java/com/zaxxer/hikari/pool/ProxyResultSet.java b/src/main/java/com/zaxxer/hikari/pool/ProxyResultSet.java new file mode 100644 index 0000000..1933979 --- /dev/null +++ b/src/main/java/com/zaxxer/hikari/pool/ProxyResultSet.java @@ -0,0 +1,106 @@ +/* + * 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 java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Wrapper; + +/** + * This is the proxy class for java.sql.ResultSet. + * + * @author Brett Wooldridge + */ +public abstract class ProxyResultSet implements ResultSet +{ + protected final ProxyConnection connection; + protected final ProxyStatement statement; + protected final ResultSet delegate; + + protected ProxyResultSet(ProxyConnection connection, ProxyStatement statement, ResultSet resultSet) + { + this.connection = connection; + this.statement = statement; + this.delegate = resultSet; + } + + final SQLException checkException(SQLException e) + { + return connection.checkException(e); + } + + /** {@inheritDoc} */ + @Override + public String toString() + { + return new StringBuilder(64) + .append(this.getClass().getSimpleName()).append('@').append(System.identityHashCode(this)) + .append(" wrapping ") + .append(delegate).toString(); + } + + // ********************************************************************** + // Overridden java.sql.ResultSet Methods + // ********************************************************************** + + /** {@inheritDoc} */ + @Override + public final Statement getStatement() throws SQLException + { + return statement; + } + + /** {@inheritDoc} */ + @Override + public void updateRow() throws SQLException + { + connection.markCommitStateDirty(); + delegate.updateRow(); + } + + /** {@inheritDoc} */ + @Override + public void insertRow() throws SQLException + { + connection.markCommitStateDirty(); + delegate.insertRow(); + } + + /** {@inheritDoc} */ + @Override + public void deleteRow() throws SQLException + { + connection.markCommitStateDirty(); + delegate.deleteRow(); + } + + /** {@inheritDoc} */ + @Override + @SuppressWarnings("unchecked") + public final <T> T unwrap(Class<T> iface) throws SQLException + { + if (iface.isInstance(delegate)) { + return (T) delegate; + } + else if (delegate instanceof Wrapper) { + return delegate.unwrap(iface); + } + + throw new SQLException("Wrapped ResultSet is not an instance of " + iface); + } +} diff --git a/src/main/java/com/zaxxer/hikari/pool/ProxyStatement.java b/src/main/java/com/zaxxer/hikari/pool/ProxyStatement.java new file mode 100644 index 0000000..1d92cd8 --- /dev/null +++ b/src/main/java/com/zaxxer/hikari/pool/ProxyStatement.java @@ -0,0 +1,240 @@ +/* + * Copyright (C) 2013 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 java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Wrapper; + +/** + * This is the proxy class for java.sql.Statement. + * + * @author Brett Wooldridge + */ +public abstract class ProxyStatement implements Statement +{ + protected final ProxyConnection connection; + protected final Statement delegate; + + private boolean isClosed; + private ResultSet proxyResultSet; + + protected ProxyStatement(ProxyConnection connection, Statement statement) + { + this.connection = connection; + this.delegate = statement; + } + + final SQLException checkException(SQLException e) + { + return connection.checkException(e); + } + + /** {@inheritDoc} */ + @Override + public final String toString() + { + final String delegateToString = delegate.toString(); + return new StringBuilder(64 + delegateToString.length()) + .append(this.getClass().getSimpleName()).append('@').append(System.identityHashCode(this)) + .append(" wrapping ") + .append(delegateToString).toString(); + } + + // ********************************************************************** + // Overridden java.sql.Statement Methods + // ********************************************************************** + + /** {@inheritDoc} */ + @Override + public final void close() throws SQLException + { + if (isClosed) { + return; + } + + isClosed = true; + connection.untrackStatement(delegate); + + try { + delegate.close(); + } + catch (SQLException e) { + throw connection.checkException(e); + } + } + + /** {@inheritDoc} */ + @Override + public Connection getConnection() throws SQLException + { + return connection; + } + + /** {@inheritDoc} */ + @Override + public boolean execute(String sql) throws SQLException + { + connection.markCommitStateDirty(); + return delegate.execute(sql); + } + + /** {@inheritDoc} */ + @Override + public boolean execute(String sql, int autoGeneratedKeys) throws SQLException + { + connection.markCommitStateDirty(); + return delegate.execute(sql, autoGeneratedKeys); + } + + /** {@inheritDoc} */ + @Override + public ResultSet executeQuery(String sql) throws SQLException + { + connection.markCommitStateDirty(); + ResultSet resultSet = delegate.executeQuery(sql); + return ProxyFactory.getProxyResultSet(connection, this, resultSet); + } + + /** {@inheritDoc} */ + @Override + public int executeUpdate(String sql) throws SQLException + { + connection.markCommitStateDirty(); + return delegate.executeUpdate(sql); + } + + /** {@inheritDoc} */ + @Override + public int[] executeBatch() throws SQLException + { + connection.markCommitStateDirty(); + return delegate.executeBatch(); + } + + /** {@inheritDoc} */ + @Override + public int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException + { + connection.markCommitStateDirty(); + return delegate.executeUpdate(sql, autoGeneratedKeys); + } + + /** {@inheritDoc} */ + @Override + public int executeUpdate(String sql, int[] columnIndexes) throws SQLException + { + connection.markCommitStateDirty(); + return delegate.executeUpdate(sql, columnIndexes); + } + + /** {@inheritDoc} */ + @Override + public int executeUpdate(String sql, String[] columnNames) throws SQLException + { + connection.markCommitStateDirty(); + return delegate.executeUpdate(sql, columnNames); + } + + /** {@inheritDoc} */ + @Override + public boolean execute(String sql, int[] columnIndexes) throws SQLException + { + connection.markCommitStateDirty(); + return delegate.execute(sql, columnIndexes); + } + + /** {@inheritDoc} */ + @Override + public boolean execute(String sql, String[] columnNames) throws SQLException + { + connection.markCommitStateDirty(); + return delegate.execute(sql, columnNames); + } + + /** {@inheritDoc} */ + @Override + public long[] executeLargeBatch() throws SQLException + { + connection.markCommitStateDirty(); + return delegate.executeLargeBatch(); + } + + /** {@inheritDoc} */ + @Override + public long executeLargeUpdate(String sql) throws SQLException + { + connection.markCommitStateDirty(); + return delegate.executeLargeUpdate(sql); + } + + /** {@inheritDoc} */ + @Override + public long executeLargeUpdate(String sql, int autoGeneratedKeys) throws SQLException + { + connection.markCommitStateDirty(); + return delegate.executeLargeUpdate(sql, autoGeneratedKeys); + } + + /** {@inheritDoc} */ + @Override + public long executeLargeUpdate(String sql, int[] columnIndexes) throws SQLException + { + connection.markCommitStateDirty(); + return delegate.executeLargeUpdate(sql, columnIndexes); + } + + /** {@inheritDoc} */ + @Override + public long executeLargeUpdate(String sql, String[] columnNames) throws SQLException + { + connection.markCommitStateDirty(); + return delegate.executeLargeUpdate(sql, columnNames); + } + + /** {@inheritDoc} */ + @Override + public ResultSet getResultSet() throws SQLException { + final ResultSet resultSet = delegate.getResultSet(); + if (resultSet != null) { + if (proxyResultSet == null || ((ProxyResultSet) proxyResultSet).delegate != resultSet) { + proxyResultSet = ProxyFactory.getProxyResultSet(connection, this, resultSet); + } + } + else { + proxyResultSet = null; + } + return proxyResultSet; + } + + /** {@inheritDoc} */ + @Override + @SuppressWarnings("unchecked") + public final <T> T unwrap(Class<T> iface) throws SQLException + { + if (iface.isInstance(delegate)) { + return (T) delegate; + } + else if (delegate instanceof Wrapper) { + return delegate.unwrap(iface); + } + + throw new SQLException("Wrapped statement is not an instance of " + iface); + } +} diff --git a/src/main/java/com/zaxxer/hikari/util/ClockSource.java b/src/main/java/com/zaxxer/hikari/util/ClockSource.java new file mode 100644 index 0000000..6d02450 --- /dev/null +++ b/src/main/java/com/zaxxer/hikari/util/ClockSource.java @@ -0,0 +1,313 @@ +/* + * Copyright (C) 2015 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.util; + +import static java.util.concurrent.TimeUnit.DAYS; +import static java.util.concurrent.TimeUnit.HOURS; +import static java.util.concurrent.TimeUnit.MICROSECONDS; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.MINUTES; +import static java.util.concurrent.TimeUnit.NANOSECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; + +import java.util.concurrent.TimeUnit; + +/** + * A resolution-independent provider of current time-stamps and elapsed time + * calculations. + * + * @author Brett Wooldridge + */ +public interface ClockSource +{ + static ClockSource CLOCK = Factory.create(); + + /** + * Get the current time-stamp (resolution is opaque). + * + * @return the current time-stamp + */ + static long currentTime() { + return CLOCK.currentTime0(); + } + + long currentTime0(); + + /** + * Convert an opaque time-stamp returned by currentTime() into + * milliseconds. + * + * @param time an opaque time-stamp returned by an instance of this class + * @return the time-stamp in milliseconds + */ + static long toMillis(long time) { + return CLOCK.toMillis0(time); + } + + long toMillis0(long time); + + /** + * Convert an opaque time-stamp returned by currentTime() into + * nanoseconds. + * + * @param time an opaque time-stamp returned by an instance of this class + * @return the time-stamp in nanoseconds + */ + static long toNanos(long time) { + return CLOCK.toNanos0(time); + } + + long toNanos0(long time); + + /** + * Convert an opaque time-stamp returned by currentTime() into an + * elapsed time in milliseconds, based on the current instant in time. + * + * @param startTime an opaque time-stamp returned by an instance of this class + * @return the elapsed time between startTime and now in milliseconds + */ + static long elapsedMillis(long startTime) { + return CLOCK.elapsedMillis0(startTime); + } + + long elapsedMillis0(long startTime); + + /** + * Get the difference in milliseconds between two opaque time-stamps returned + * by currentTime(). + * + * @param startTime an opaque time-stamp returned by an instance of this class + * @param endTime an opaque time-stamp returned by an instance of this class + * @return the elapsed time between startTime and endTime in milliseconds + */ + static long elapsedMillis(long startTime, long endTime) { + return CLOCK.elapsedMillis0(startTime, endTime); + } + + long elapsedMillis0(long startTime, long endTime); + + /** + * Convert an opaque time-stamp returned by currentTime() into an + * elapsed time in milliseconds, based on the current instant in time. + * + * @param startTime an opaque time-stamp returned by an instance of this class + * @return the elapsed time between startTime and now in milliseconds + */ + static long elapsedNanos(long startTime) { + return CLOCK.elapsedNanos0(startTime); + } + + long elapsedNanos0(long startTime); + + /** + * Get the difference in nanoseconds between two opaque time-stamps returned + * by currentTime(). + * + * @param startTime an opaque time-stamp returned by an instance of this class + * @param endTime an opaque time-stamp returned by an instance of this class + * @return the elapsed time between startTime and endTime in nanoseconds + */ + static long elapsedNanos(long startTime, long endTime) { + return CLOCK.elapsedNanos0(startTime, endTime); + } + + long elapsedNanos0(long startTime, long endTime); + + /** + * Return the specified opaque time-stamp plus the specified number of milliseconds. + * + * @param time an opaque time-stamp + * @param millis milliseconds to add + * @return a new opaque time-stamp + */ + static long plusMillis(long time, long millis) { + return CLOCK.plusMillis0(time, millis); + } + + long plusMillis0(long time, long millis); + + /** + * Get the TimeUnit the ClockSource is denominated in. + * @return + */ + static TimeUnit getSourceTimeUnit() { + return CLOCK.getSourceTimeUnit0(); + } + + TimeUnit getSourceTimeUnit0(); + + /** + * Get a String representation of the elapsed time in appropriate magnitude terminology. + * + * @param startTime an opaque time-stamp + * @param endTime an opaque time-stamp + * @return a string representation of the elapsed time interval + */ + static String elapsedDisplayString(long startTime, long endTime) { + return CLOCK.elapsedDisplayString0(startTime, endTime); + } + + default String elapsedDisplayString0(long startTime, long endTime) { + long elapsedNanos = elapsedNanos0(startTime, endTime); + + StringBuilder sb = new StringBuilder(elapsedNanos < 0 ? "-" : ""); + elapsedNanos = Math.abs(elapsedNanos); + + for (TimeUnit unit : TIMEUNITS_DESCENDING) { + long converted = unit.convert(elapsedNanos, NANOSECONDS); + if (converted > 0) { + sb.append(converted).append(TIMEUNIT_DISPLAY_VALUES[unit.ordinal()]); + elapsedNanos -= NANOSECONDS.convert(converted, unit); + } + } + + return sb.toString(); + } + + TimeUnit[] TIMEUNITS_DESCENDING = {DAYS, HOURS, MINUTES, SECONDS, MILLISECONDS, MICROSECONDS, NANOSECONDS}; + + String[] TIMEUNIT_DISPLAY_VALUES = {"ns", "µs", "ms", "s", "m", "h", "d"}; + + /** + * Factory class used to create a platform-specific ClockSource. + */ + class Factory + { + private static ClockSource create() { + String os = System.getProperty("os.name"); + if ("Mac OS X".equals(os)) { + return new MillisecondClockSource(); + } + + return new NanosecondClockSource(); + } + } + + final class MillisecondClockSource implements ClockSource + { + /** {@inheritDoc} */ + @Override + public long currentTime0() { + return System.currentTimeMillis(); + } + + /** {@inheritDoc} */ + @Override + public long elapsedMillis0(final long startTime) { + return System.currentTimeMillis() - startTime; + } + + /** {@inheritDoc} */ + @Override + public long elapsedMillis0(final long startTime, final long endTime) { + return endTime - startTime; + } + + /** {@inheritDoc} */ + @Override + public long elapsedNanos0(final long startTime) { + return MILLISECONDS.toNanos(System.currentTimeMillis() - startTime); + } + + /** {@inheritDoc} */ + @Override + public long elapsedNanos0(final long startTime, final long endTime) { + return MILLISECONDS.toNanos(endTime - startTime); + } + + /** {@inheritDoc} */ + @Override + public long toMillis0(final long time) { + return time; + } + + /** {@inheritDoc} */ + @Override + public long toNanos0(final long time) { + return MILLISECONDS.toNanos(time); + } + + /** {@inheritDoc} */ + @Override + public long plusMillis0(final long time, final long millis) { + return time + millis; + } + + /** {@inheritDoc} */ + @Override + public TimeUnit getSourceTimeUnit0() { + return MILLISECONDS; + } + } + + class NanosecondClockSource implements ClockSource + { + /** {@inheritDoc} */ + @Override + public long currentTime0() { + return System.nanoTime(); + } + + /** {@inheritDoc} */ + @Override + public long toMillis0(final long time) { + return NANOSECONDS.toMillis(time); + } + + /** {@inheritDoc} */ + @Override + public long toNanos0(final long time) { + return time; + } + + /** {@inheritDoc} */ + @Override + public long elapsedMillis0(final long startTime) { + return NANOSECONDS.toMillis(System.nanoTime() - startTime); + } + + /** {@inheritDoc} */ + @Override + public long elapsedMillis0(final long startTime, final long endTime) { + return NANOSECONDS.toMillis(endTime - startTime); + } + + /** {@inheritDoc} */ + @Override + public long elapsedNanos0(final long startTime) { + return System.nanoTime() - startTime; + } + + /** {@inheritDoc} */ + @Override + public long elapsedNanos0(final long startTime, final long endTime) { + return endTime - startTime; + } + + /** {@inheritDoc} */ + @Override + public long plusMillis0(final long time, final long millis) { + return time + MILLISECONDS.toNanos(millis); + } + + /** {@inheritDoc} */ + @Override + public TimeUnit getSourceTimeUnit0() { + return NANOSECONDS; + } + } +} diff --git a/src/main/java/com/zaxxer/hikari/util/ConcurrentBag.java b/src/main/java/com/zaxxer/hikari/util/ConcurrentBag.java new file mode 100755 index 0000000..ac0ccb1 --- /dev/null +++ b/src/main/java/com/zaxxer/hikari/util/ConcurrentBag.java @@ -0,0 +1,385 @@ +/* + * 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.util; + +import static com.zaxxer.hikari.util.ClockSource.currentTime; +import static com.zaxxer.hikari.util.ClockSource.elapsedNanos; +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.ConcurrentBag.IConcurrentBagEntry.STATE_RESERVED; +import static java.lang.Thread.yield; +import static java.util.concurrent.TimeUnit.NANOSECONDS; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Future; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.zaxxer.hikari.util.ConcurrentBag.IConcurrentBagEntry; + +/** + * This is a specialized concurrent bag that achieves superior performance + * to LinkedBlockingQueue and LinkedTransferQueue for the purposes of a + * connection pool. It uses ThreadLocal storage when possible to avoid + * locks, but resorts to scanning a common collection if there are no + * available items in the ThreadLocal list. Not-in-use items in the + * ThreadLocal lists can be "stolen" when the borrowing thread has none + * of its own. It is a "lock-less" implementation using a specialized + * AbstractQueuedLongSynchronizer to manage cross-thread signaling. + * + * Note that items that are "borrowed" from the bag are not actually + * removed from any collection, so garbage collection will not occur + * even if the reference is abandoned. Thus care must be taken to + * "requite" borrowed objects otherwise a memory leak will result. Only + * the "remove" method can completely remove an object from the bag. + * + * @author Brett Wooldridge + * + * @param <T> the templated type to store in the bag + */ +public class ConcurrentBag<T extends IConcurrentBagEntry> implements AutoCloseable +{ + private static final Logger LOGGER = LoggerFactory.getLogger(ConcurrentBag.class); + + private final CopyOnWriteArrayList<T> sharedList; + private final boolean weakThreadLocals; + + private final ThreadLocal<List<Object>> threadList; + private final IBagStateListener listener; + private final AtomicInteger waiters; + private volatile boolean closed; + + private final SynchronousQueue<T> handoffQueue; + + public static interface IConcurrentBagEntry + { + int STATE_NOT_IN_USE = 0; + int STATE_IN_USE = 1; + int STATE_REMOVED = -1; + int STATE_RESERVED = -2; + + boolean compareAndSet(int expectState, int newState); + void setState(int newState); + int getState(); + } + + public static interface IBagStateListener + { + Future<Boolean> addBagItem(int waiting); + } + + /** + * Construct a ConcurrentBag with the specified listener. + * + * @param listener the IBagStateListener to attach to this bag + */ + public ConcurrentBag(final IBagStateListener listener) + { + this.listener = listener; + this.weakThreadLocals = useWeakThreadLocals(); + + this.handoffQueue = new SynchronousQueue<>(true); + this.waiters = new AtomicInteger(); + this.sharedList = new CopyOnWriteArrayList<>(); + if (weakThreadLocals) { + this.threadList = ThreadLocal.withInitial(() -> new ArrayList<>(16)); + } + else { + this.threadList = ThreadLocal.withInitial(() -> new FastList<>(IConcurrentBagEntry.class, 16)); + } + } + + /** + * The method will borrow a BagEntry from the bag, blocking for the + * specified timeout if none are available. + * + * @param timeout how long to wait before giving up, in units of unit + * @param timeUnit a <code>TimeUnit</code> determining how to interpret the timeout parameter + * @return a borrowed instance from the bag or null if a timeout occurs + * @throws InterruptedException if interrupted while waiting + */ + public T borrow(long timeout, final TimeUnit timeUnit) throws InterruptedException + { + // Try the thread-local list first + final List<Object> list = threadList.get(); + for (int i = list.size() - 1; i >= 0; i--) { + final Object entry = list.remove(i); + @SuppressWarnings("unchecked") + final T bagEntry = weakThreadLocals ? ((WeakReference<T>) entry).get() : (T) entry; + if (bagEntry != null && bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) { + return bagEntry; + } + } + + // Otherwise, scan the shared list ... then poll the handoff queue + final int waiting = waiters.incrementAndGet(); + try { + for (T bagEntry : sharedList) { + if (bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) { + // If we may have stolen another waiter's connection, request another bag add. + if (waiting > 1) { + listener.addBagItem(waiting - 1); + } + return bagEntry; + } + } + + listener.addBagItem(waiting); + + timeout = timeUnit.toNanos(timeout); + do { + final long start = currentTime(); + final T bagEntry = handoffQueue.poll(timeout, NANOSECONDS); + if (bagEntry == null || bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) { + return bagEntry; + } + + timeout -= elapsedNanos(start); + } while (timeout > 10_000); + + return null; + } + finally { + waiters.decrementAndGet(); + } + } + + /** + * This method will return a borrowed object to the bag. Objects + * that are borrowed from the bag but never "requited" will result + * in a memory leak. + * + * @param bagEntry the value to return to the bag + * @throws NullPointerException if value is null + * @throws IllegalStateException if the bagEntry was not borrowed from the bag + */ + public void requite(final T bagEntry) + { + bagEntry.setState(STATE_NOT_IN_USE); + + while (waiters.get() > 0) { + if (handoffQueue.offer(bagEntry)) { + return; + } + yield(); + } + + final List<Object> threadLocalList = threadList.get(); + threadLocalList.add(weakThreadLocals ? new WeakReference<>(bagEntry) : bagEntry); + } + + /** + * Add a new object to the bag for others to borrow. + * + * @param bagEntry an object to add to the bag + */ + public void add(final T bagEntry) + { + if (closed) { + LOGGER.info("ConcurrentBag has been closed, ignoring add()"); + throw new IllegalStateException("ConcurrentBag has been closed, ignoring add()"); + } + + sharedList.add(bagEntry); + + // spin until a thread takes it or none are waiting + while (waiters.get() > 0 && !handoffQueue.offer(bagEntry)) { + yield(); + } + } + + /** + * Remove a value from the bag. This method should only be called + * with objects obtained by <code>borrow(long, TimeUnit)</code> or <code>reserve(T)</code> + * + * @param bagEntry the value to remove + * @return true if the entry was removed, false otherwise + * @throws IllegalStateException if an attempt is made to remove an object + * from the bag that was not borrowed or reserved first + */ + public boolean remove(final T bagEntry) + { + if (!bagEntry.compareAndSet(STATE_IN_USE, STATE_REMOVED) && !bagEntry.compareAndSet(STATE_RESERVED, STATE_REMOVED) && !closed) { + LOGGER.warn("Attempt to remove an object from the bag that was not borrowed or reserved: {}", bagEntry); + return false; + } + + final boolean removed = sharedList.remove(bagEntry); + if (!removed && !closed) { + LOGGER.warn("Attempt to remove an object from the bag that does not exist: {}", bagEntry); + } + + return removed; + } + + /** + * Close the bag to further adds. + */ + @Override + public void close() + { + closed = true; + } + + /** + * This method provides a "snapshot" in time of the BagEntry + * items in the bag in the specified state. It does not "lock" + * or reserve items in any way. Call <code>reserve(T)</code> + * on items in list before performing any action on them. + * + * @param state one of the {@link IConcurrentBagEntry} states + * @return a possibly empty list of objects having the state specified + */ + public List<T> values(final int state) + { + return sharedList.stream().filter(e -> e.getState() == state).collect(Collectors.toList()); + } + + /** + * This method provides a "snapshot" in time of the bag items. It + * does not "lock" or reserve items in any way. Call <code>reserve(T)</code> + * on items in the list, or understand the concurrency implications of + * modifying items, before performing any action on them. + * + * @return a possibly empty list of (all) bag items + */ + @SuppressWarnings("unchecked") + public List<T> values() + { + return (List<T>) sharedList.clone(); + } + + /** + * The method is used to make an item in the bag "unavailable" for + * borrowing. It is primarily used when wanting to operate on items + * returned by the <code>values(int)</code> method. Items that are + * reserved can be removed from the bag via <code>remove(T)</code> + * without the need to unreserve them. Items that are not removed + * from the bag can be make available for borrowing again by calling + * the <code>unreserve(T)</code> method. + * + * @param bagEntry the item to reserve + * @return true if the item was able to be reserved, false otherwise + */ + public boolean reserve(final T bagEntry) + { + return bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_RESERVED); + } + + /** + * This method is used to make an item reserved via <code>reserve(T)</code> + * available again for borrowing. + * + * @param bagEntry the item to unreserve + */ + public void unreserve(final T bagEntry) + { + if (bagEntry.compareAndSet(STATE_RESERVED, STATE_NOT_IN_USE)) { + // spin until a thread takes it or none are waiting + while (waiters.get() > 0 && !handoffQueue.offer(bagEntry)) { + yield(); + } + } + else { + LOGGER.warn("Attempt to relinquish an object to the bag that was not reserved: {}", bagEntry); + } + } + + /** + * Get the number of threads pending (waiting) for an item from the + * bag to become available. + * + * @return the number of threads waiting for items from the bag + */ + public int getWaitingThreadCount() + { + return waiters.get(); + } + + /** + * Get a count of the number of items in the specified state at the time of this call. + * + * @param state the state of the items to count + * @return a count of how many items in the bag are in the specified state + */ + public int getCount(final int state) + { + int count = 0; + for (IConcurrentBagEntry e : sharedList) { + if (e.getState() == state) { + count++; + } + } + return count; + } + + public int[] getStateCounts() + { + final int[] states = new int[6]; + for (IConcurrentBagEntry e : sharedList) { + ++states[e.getState()]; + } + states[4] = sharedList.size(); + states[5] = waiters.get(); + + return states; + } + + /** + * Get the total number of items in the bag. + * + * @return the number of items in the bag + */ + public int size() + { + return sharedList.size(); + } + + public void dumpState() + { + sharedList.forEach(entry -> LOGGER.info(entry.toString())); + } + + /** + * Determine whether to use WeakReferences based on whether there is a + * custom ClassLoader implementation sitting between this class and the + * System ClassLoader. + * + * @return true if we should use WeakReferences in our ThreadLocals, false otherwise + */ + private boolean useWeakThreadLocals() + { + try { + if (System.getProperty("com.zaxxer.hikari.useWeakReferences") != null) { // undocumented manual override of WeakReference behavior + return Boolean.getBoolean("com.zaxxer.hikari.useWeakReferences"); + } + + return getClass().getClassLoader() != ClassLoader.getSystemClassLoader(); + } + catch (SecurityException se) { + return true; + } + } +} diff --git a/src/main/java/com/zaxxer/hikari/util/DriverDataSource.java b/src/main/java/com/zaxxer/hikari/util/DriverDataSource.java new file mode 100644 index 0000000..9aa9b6b --- /dev/null +++ b/src/main/java/com/zaxxer/hikari/util/DriverDataSource.java @@ -0,0 +1,145 @@ +/* + * 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.util; + +import java.io.PrintWriter; +import java.sql.Connection; +import java.sql.Driver; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.util.Enumeration; +import java.util.Map.Entry; +import java.util.Properties; + +import javax.sql.DataSource; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class DriverDataSource implements DataSource +{ + private static final Logger LOGGER = LoggerFactory.getLogger(DriverDataSource.class); + + private final String jdbcUrl; + private final Properties driverProperties; + private Driver driver; + + public DriverDataSource(String jdbcUrl, String driverClassName, Properties properties, String username, String password) + { + this.jdbcUrl = jdbcUrl; + this.driverProperties = new Properties(); + + for (Entry<Object, Object> entry : properties.entrySet()) { + driverProperties.setProperty(entry.getKey().toString(), entry.getValue().toString()); + } + + if (username != null) { + driverProperties.put("user", driverProperties.getProperty("user", username)); + } + if (password != null) { + driverProperties.put("password", driverProperties.getProperty("password", password)); + } + + if (driverClassName != null) { + Enumeration<Driver> drivers = DriverManager.getDrivers(); + while (drivers.hasMoreElements()) { + Driver d = drivers.nextElement(); + if (d.getClass().getName().equals(driverClassName)) { + driver = d; + break; + } + } + + if (driver == null) { + LOGGER.warn("Registered driver with driverClassName={} was not found, trying direct instantiation.", driverClassName); + try { + Class<?> driverClass = this.getClass().getClassLoader().loadClass(driverClassName); + driver = (Driver) driverClass.newInstance(); + } + catch (Exception e) { + LOGGER.warn("Failed to create instance of driver class {}, trying jdbcUrl resolution", driverClassName, e); + } + } + } + + try { + if (driver == null) { + driver = DriverManager.getDriver(jdbcUrl); + } + else if (!driver.acceptsURL(jdbcUrl)) { + throw new RuntimeException("Driver " + driverClassName + " claims to not accept jdbcUrl, " + jdbcUrl); + } + } + catch (SQLException e) { + throw new RuntimeException("Failed to get driver instance for jdbcUrl=" + jdbcUrl, e); + } + } + + @Override + public Connection getConnection() throws SQLException + { + return driver.connect(jdbcUrl, driverProperties); + } + + @Override + public Connection getConnection(String username, String password) throws SQLException + { + return getConnection(); + } + + @Override + public PrintWriter getLogWriter() throws SQLException + { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void setLogWriter(PrintWriter logWriter) throws SQLException + { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public void setLoginTimeout(int seconds) throws SQLException + { + DriverManager.setLoginTimeout(seconds); + } + + @Override + public int getLoginTimeout() throws SQLException + { + return DriverManager.getLoginTimeout(); + } + + @Override + public java.util.logging.Logger getParentLogger() throws SQLFeatureNotSupportedException + { + return driver.getParentLogger(); + } + + @Override + public <T> T unwrap(Class<T> iface) throws SQLException + { + throw new SQLFeatureNotSupportedException(); + } + + @Override + public boolean isWrapperFor(Class<?> iface) throws SQLException + { + return false; + } +} diff --git a/src/main/java/com/zaxxer/hikari/util/FastList.java b/src/main/java/com/zaxxer/hikari/util/FastList.java new file mode 100644 index 0000000..a1d42b2 --- /dev/null +++ b/src/main/java/com/zaxxer/hikari/util/FastList.java @@ -0,0 +1,368 @@ +/* + * 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.util; + +import java.io.Serializable; +import java.lang.reflect.Array; +import java.util.Collection; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.NoSuchElementException; +import java.util.RandomAccess; +import java.util.Spliterator; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.function.UnaryOperator; + +/** + * Fast list without range checking. + * + * @author Brett Wooldridge + */ +public final class FastList<T> implements List<T>, RandomAccess, Serializable +{ + private static final long serialVersionUID = -4598088075242913858L; + + private final Class<?> clazz; + private T[] elementData; + private int size; + + /** + * Construct a FastList with a default size of 32. + * @param clazz the Class stored in the collection + */ + @SuppressWarnings("unchecked") + public FastList(Class<?> clazz) + { + this.elementData = (T[]) Array.newInstance(clazz, 32); + this.clazz = clazz; + } + + /** + * Construct a FastList with a specified size. + * @param clazz the Class stored in the collection + * @param capacity the initial size of the FastList + */ + @SuppressWarnings("unchecked") + public FastList(Class<?> clazz, int capacity) + { + this.elementData = (T[]) Array.newInstance(clazz, capacity); + this.clazz = clazz; + } + + /** + * Add an element to the tail of the FastList. + * + * @param element the element to add + */ + @Override + public boolean add(T element) + { + if (size < elementData.length) { + elementData[size++] = element; + } + else { + // overflow-conscious code + final int oldCapacity = elementData.length; + final int newCapacity = oldCapacity << 1; + @SuppressWarnings("unchecked") + final T[] newElementData = (T[]) Array.newInstance(clazz, newCapacity); + System.arraycopy(elementData, 0, newElementData, 0, oldCapacity); + newElementData[size++] = element; + elementData = newElementData; + } + + return true; + } + + /** + * Get the element at the specified index. + * + * @param index the index of the element to get + * @return the element, or ArrayIndexOutOfBounds is thrown if the index is invalid + */ + @Override + public T get(int index) + { + return elementData[index]; + } + + /** + * Remove the last element from the list. No bound check is performed, so if this + * method is called on an empty list and ArrayIndexOutOfBounds exception will be + * thrown. + * + * @return the last element of the list + */ + public T removeLast() + { + T element = elementData[--size]; + elementData[size] = null; + return element; + } + + /** + * This remove method is most efficient when the element being removed + * is the last element. Equality is identity based, not equals() based. + * Only the first matching element is removed. + * + * @param element the element to remove + */ + @Override + public boolean remove(Object element) + { + for (int index = size - 1; index >= 0; index--) { + if (element == elementData[index]) { + final int numMoved = size - index - 1; + if (numMoved > 0) { + System.arraycopy(elementData, index + 1, elementData, index, numMoved); + } + elementData[--size] = null; + return true; + } + } + + return false; + } + + /** + * Clear the FastList. + */ + @Override + public void clear() + { + for (int i = 0; i < size; i++) { + elementData[i] = null; + } + + size = 0; + } + + /** + * Get the current number of elements in the FastList. + * + * @return the number of current elements + */ + @Override + public int size() + { + return size; + } + + /** {@inheritDoc} */ + @Override + public boolean isEmpty() + { + return size == 0; + } + + /** {@inheritDoc} */ + @Override + public T set(int index, T element) + { + T old = elementData[index]; + elementData[index] = element; + return old; + } + + /** {@inheritDoc} */ + @Override + public T remove(int index) + { + if (size == 0) { + return null; + } + + final T old = elementData[index]; + + final int numMoved = size - index - 1; + if (numMoved > 0) { + System.arraycopy(elementData, index + 1, elementData, index, numMoved); + } + + elementData[--size] = null; + + return old; + } + + /** {@inheritDoc} */ + @Override + public boolean contains(Object o) + { + throw new UnsupportedOperationException(); + } + + /** {@inheritDoc} */ + @Override + public Iterator<T> iterator() + { + return new Iterator<T>() { + private int index; + + @Override + public boolean hasNext() + { + return index < size; + } + + @Override + public T next() + { + if (index < size) { + return elementData[index++]; + } + + throw new NoSuchElementException("No more elements in FastList"); + } + }; + } + + /** {@inheritDoc} */ + @Override + public Object[] toArray() + { + throw new UnsupportedOperationException(); + } + + /** {@inheritDoc} */ + @Override + public <E> E[] toArray(E[] a) + { + throw new UnsupportedOperationException(); + } + + /** {@inheritDoc} */ + @Override + public boolean containsAll(Collection<?> c) + { + throw new UnsupportedOperationException(); + } + + /** {@inheritDoc} */ + @Override + public boolean addAll(Collection<? extends T> c) + { + throw new UnsupportedOperationException(); + } + + /** {@inheritDoc} */ + @Override + public boolean addAll(int index, Collection<? extends T> c) + { + throw new UnsupportedOperationException(); + } + + /** {@inheritDoc} */ + @Override + public boolean removeAll(Collection<?> c) + { + throw new UnsupportedOperationException(); + } + + /** {@inheritDoc} */ + @Override + public boolean retainAll(Collection<?> c) + { + throw new UnsupportedOperationException(); + } + + /** {@inheritDoc} */ + @Override + public void add(int index, T element) + { + throw new UnsupportedOperationException(); + } + + /** {@inheritDoc} */ + @Override + public int indexOf(Object o) + { + throw new UnsupportedOperationException(); + } + + /** {@inheritDoc} */ + @Override + public int lastIndexOf(Object o) + { + throw new UnsupportedOperationException(); + } + + /** {@inheritDoc} */ + @Override + public ListIterator<T> listIterator() + { + throw new UnsupportedOperationException(); + } + + /** {@inheritDoc} */ + @Override + public ListIterator<T> listIterator(int index) + { + throw new UnsupportedOperationException(); + } + + /** {@inheritDoc} */ + @Override + public List<T> subList(int fromIndex, int toIndex) + { + throw new UnsupportedOperationException(); + } + + /** {@inheritDoc} */ + @Override + public Object clone() + { + throw new UnsupportedOperationException(); + } + + /** {@inheritDoc} */ + @Override + public void forEach(Consumer<? super T> action) + { + throw new UnsupportedOperationException(); + } + + /** {@inheritDoc} */ + @Override + public Spliterator<T> spliterator() + { + throw new UnsupportedOperationException(); + } + + /** {@inheritDoc} */ + @Override + public boolean removeIf(Predicate<? super T> filter) + { + throw new UnsupportedOperationException(); + } + + /** {@inheritDoc} */ + @Override + public void replaceAll(UnaryOperator<T> operator) + { + throw new UnsupportedOperationException(); + } + + /** {@inheritDoc} */ + @Override + public void sort(Comparator<? super T> c) + { + throw new UnsupportedOperationException(); + } +} diff --git a/src/main/java/com/zaxxer/hikari/util/JavassistProxyFactory.java b/src/main/java/com/zaxxer/hikari/util/JavassistProxyFactory.java new file mode 100644 index 0000000..c1b2cb0 --- /dev/null +++ b/src/main/java/com/zaxxer/hikari/util/JavassistProxyFactory.java @@ -0,0 +1,278 @@ +/* + * 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.util; + +import java.lang.reflect.Array; +import java.sql.CallableStatement; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import com.zaxxer.hikari.pool.ProxyCallableStatement; +import com.zaxxer.hikari.pool.ProxyConnection; +import com.zaxxer.hikari.pool.ProxyFactory; +import com.zaxxer.hikari.pool.ProxyPreparedStatement; +import com.zaxxer.hikari.pool.ProxyResultSet; +import com.zaxxer.hikari.pool.ProxyStatement; + +import javassist.ClassPool; +import javassist.CtClass; +import javassist.CtMethod; +import javassist.CtNewMethod; +import javassist.LoaderClassPath; +import javassist.Modifier; +import javassist.NotFoundException; +import javassist.bytecode.ClassFile; + +/** + * This class generates the proxy objects for {@link Connection}, {@link Statement}, + * {@link PreparedStatement}, and {@link CallableStatement}. Additionally it injects + * method bodies into the {@link ProxyFactory} class methods that can instantiate + * instances of the generated proxies. + * + * @author Brett Wooldridge + */ +public final class JavassistProxyFactory +{ + private static ClassPool classPool; + + public static void main(String... args) + { + classPool = new ClassPool(); + classPool.importPackage("java.sql"); + classPool.appendClassPath(new LoaderClassPath(JavassistProxyFactory.class.getClassLoader())); + + try { + // Cast is not needed for these + String methodBody = "{ try { return delegate.method($$); } catch (SQLException e) { throw checkException(e); } }"; + generateProxyClass(Connection.class, ProxyConnection.class.getName(), methodBody); + generateProxyClass(Statement.class, ProxyStatement.class.getName(), methodBody); + generateProxyClass(ResultSet.class, ProxyResultSet.class.getName(), methodBody); + + // For these we have to cast the delegate + methodBody = "{ try { return ((cast) delegate).method($$); } catch (SQLException e) { throw checkException(e); } }"; + generateProxyClass(PreparedStatement.class, ProxyPreparedStatement.class.getName(), methodBody); + generateProxyClass(CallableStatement.class, ProxyCallableStatement.class.getName(), methodBody); + + modifyProxyFactory(); + } + catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static void modifyProxyFactory() throws Exception + { + System.out.println("Generating method bodies for com.zaxxer.hikari.proxy.ProxyFactory"); + + String packageName = ProxyConnection.class.getPackage().getName(); + CtClass proxyCt = classPool.getCtClass("com.zaxxer.hikari.pool.ProxyFactory"); + for (CtMethod method : proxyCt.getMethods()) { + switch (method.getName()) { + case "getProxyConnection": + method.setBody("{return new " + packageName + ".HikariProxyConnection($$);}"); + break; + case "getProxyStatement": + method.setBody("{return new " + packageName + ".HikariProxyStatement($$);}"); + break; + case "getProxyPreparedStatement": + method.setBody("{return new " + packageName + ".HikariProxyPreparedStatement($$);}"); + break; + case "getProxyCallableStatement": + method.setBody("{return new " + packageName + ".HikariProxyCallableStatement($$);}"); + break; + case "getProxyResultSet": + method.setBody("{return new " + packageName + ".HikariProxyResultSet($$);}"); + break; + default: + // unhandled method + break; + } + } + + proxyCt.writeFile("target/classes"); + } + + /** + * Generate Javassist Proxy Classes + */ + private static <T> void generateProxyClass(Class<T> primaryInterface, String superClassName, String methodBody) throws Exception + { + String newClassName = superClassName.replaceAll("(.+)\\.(\\w+)", "$1.Hikari$2"); + + CtClass superCt = classPool.getCtClass(superClassName); + CtClass targetCt = classPool.makeClass(newClassName, superCt); + targetCt.setModifiers(Modifier.FINAL); + + System.out.println("Generating " + newClassName); + + targetCt.setModifiers(Modifier.PUBLIC); + + // Make a set of method signatures we inherit implementation for, so we don't generate delegates for these + Set<String> superSigs = new HashSet<>(); + for (CtMethod method : superCt.getMethods()) { + if ((method.getModifiers() & Modifier.FINAL) == Modifier.FINAL) { + superSigs.add(method.getName() + method.getSignature()); + } + } + + Set<String> methods = new HashSet<>(); + Set<Class<?>> interfaces = getAllInterfaces(primaryInterface); + for (Class<?> intf : interfaces) { + CtClass intfCt = classPool.getCtClass(intf.getName()); + targetCt.addInterface(intfCt); + for (CtMethod intfMethod : intfCt.getDeclaredMethods()) { + final String signature = intfMethod.getName() + intfMethod.getSignature(); + + // don't generate delegates for methods we override + if (superSigs.contains(signature)) { + continue; + } + + // Ignore already added methods that come from other interfaces + if (methods.contains(signature)) { + continue; + } + + // Track what methods we've added + methods.add(signature); + + // Clone the method we want to inject into + CtMethod method = CtNewMethod.copy(intfMethod, targetCt, null); + + String modifiedBody = methodBody; + + // If the super-Proxy has concrete methods (non-abstract), transform the call into a simple super.method() call + CtMethod superMethod = superCt.getMethod(intfMethod.getName(), intfMethod.getSignature()); + if ((superMethod.getModifiers() & Modifier.ABSTRACT) != Modifier.ABSTRACT && !isDefaultMethod(intf, intfCt, intfMethod)) { + modifiedBody = modifiedBody.replace("((cast) ", ""); + modifiedBody = modifiedBody.replace("delegate", "super"); + modifiedBody = modifiedBody.replace("super)", "super"); + } + + modifiedBody = modifiedBody.replace("cast", primaryInterface.getName()); + + // Generate a method that simply invokes the same method on the delegate + if (isThrowsSqlException(intfMethod)) { + modifiedBody = modifiedBody.replace("method", method.getName()); + } + else { + modifiedBody = "{ return ((cast) delegate).method($$); }".replace("method", method.getName()).replace("cast", primaryInterface.getName()); + } + + if (method.getReturnType() == CtClass.voidType) { + modifiedBody = modifiedBody.replace("return", ""); + } + + method.setBody(modifiedBody); + targetCt.addMethod(method); + } + } + + targetCt.getClassFile().setMajorVersion(ClassFile.JAVA_7); + targetCt.writeFile("target/classes"); + } + + private static boolean isThrowsSqlException(CtMethod method) + { + try { + for (CtClass clazz : method.getExceptionTypes()) { + if (clazz.getSimpleName().equals("SQLException")) { + return true; + } + } + } + catch (NotFoundException e) { + // fall thru + } + + return false; + } + + private static boolean isDefaultMethod(Class<?> intf, CtClass intfCt, CtMethod intfMethod) throws Exception + { + List<Class<?>> paramTypes = new ArrayList<>(); + + for (CtClass pt : intfMethod.getParameterTypes()) { + paramTypes.add(toJavaClass(pt)); + } + + return intf.getDeclaredMethod(intfMethod.getName(), paramTypes.toArray(new Class[paramTypes.size()])).toString().contains("default "); + } + + private static Set<Class<?>> getAllInterfaces(Class<?> clazz) + { + Set<Class<?>> interfaces = new HashSet<>(); + for (Class<?> intf : Arrays.asList(clazz.getInterfaces())) { + if (intf.getInterfaces().length > 0) { + interfaces.addAll(getAllInterfaces(intf)); + } + interfaces.add(intf); + } + if (clazz.getSuperclass() != null) { + interfaces.addAll(getAllInterfaces(clazz.getSuperclass())); + } + + if (clazz.isInterface()) { + interfaces.add(clazz); + } + + return interfaces; + } + + private static Class<?> toJavaClass(CtClass cls) throws Exception + { + if (cls.getName().endsWith("[]")) { + return Array.newInstance(toJavaClass(cls.getName().replace("[]", "")), 0).getClass(); + } + else { + return toJavaClass(cls.getName()); + } + } + + private static Class<?> toJavaClass(String cn) throws Exception + { + switch (cn) { + case "int": + return int.class; + case "long": + return long.class; + case "short": + return short.class; + case "byte": + return byte.class; + case "float": + return float.class; + case "double": + return double.class; + case "boolean": + return boolean.class; + case "char": + return char.class; + case "void": + return void.class; + default: + return Class.forName(cn); + } + } +} diff --git a/src/main/java/com/zaxxer/hikari/util/PropertyElf.java b/src/main/java/com/zaxxer/hikari/util/PropertyElf.java new file mode 100644 index 0000000..f03b8c6 --- /dev/null +++ b/src/main/java/com/zaxxer/hikari/util/PropertyElf.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2013 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.util; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Properties; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.zaxxer.hikari.HikariConfig; + +/** + * A class that reflectively sets bean properties on a target object. + * + * @author Brett Wooldridge + */ +public final class PropertyElf +{ + private static final Logger LOGGER = LoggerFactory.getLogger(PropertyElf.class); + + private static final Pattern GETTER_PATTERN = Pattern.compile("(get|is)[A-Z].+"); + + public static void setTargetFromProperties(final Object target, final Properties properties) + { + if (target == null || properties == null) { + return; + } + + List<Method> methods = Arrays.asList(target.getClass().getMethods()); + properties.forEach((key, value) -> { + if (target instanceof HikariConfig && key.toString().startsWith("dataSource.")) { + ((HikariConfig) target).addDataSourceProperty(key.toString().substring("dataSource.".length()), value); + } + else { + setProperty(target, key.toString(), value, methods); + } + }); + } + + /** + * Get the bean-style property names for the specified object. + * + * @param targetClass the target object + * @return a set of property names + */ + public static Set<String> getPropertyNames(final Class<?> targetClass) + { + HashSet<String> set = new HashSet<>(); + Matcher matcher = GETTER_PATTERN.matcher(""); + for (Method method : targetClass.getMethods()) { + String name = method.getName(); + if (method.getParameterTypes().length == 0 && matcher.reset(name).matches()) { + name = name.replaceFirst("(get|is)", ""); + try { + if (targetClass.getMethod("set" + name, method.getReturnType()) != null) { + name = Character.toLowerCase(name.charAt(0)) + name.substring(1); + set.add(name); + } + } + catch (Exception e) { + continue; + } + } + } + + return set; + } + + public static Object getProperty(final String propName, final Object target) + { + try { + // use the english locale to avoid the infamous turkish locale bug + String capitalized = "get" + propName.substring(0, 1).toUpperCase(Locale.ENGLISH) + propName.substring(1); + Method method = target.getClass().getMethod(capitalized); + return method.invoke(target); + } + catch (Exception e) { + try { + String capitalized = "is" + propName.substring(0, 1).toUpperCase(Locale.ENGLISH) + propName.substring(1); + Method method = target.getClass().getMethod(capitalized); + return method.invoke(target); + } + catch (Exception e2) { + return null; + } + } + } + + public static Properties copyProperties(final Properties props) + { + Properties copy = new Properties(); + props.forEach((key, value) -> copy.setProperty(key.toString(), value.toString())); + return copy; + } + + private static void setProperty(final Object target, final String propName, final Object propValue, final List<Method> methods) + { + // use the english locale to avoid the infamous turkish locale bug + String methodName = "set" + propName.substring(0, 1).toUpperCase(Locale.ENGLISH) + propName.substring(1); + Method writeMethod = methods.stream().filter(m -> m.getName().equals(methodName) && m.getParameterCount() == 1).findFirst().orElse(null); + + if (writeMethod == null) { + String methodName2 = "set" + propName.toUpperCase(Locale.ENGLISH); + writeMethod = methods.stream().filter(m -> m.getName().equals(methodName2) && m.getParameterCount() == 1).findFirst().orElse(null); + } + + if (writeMethod == null) { + LOGGER.error("Property {} does not exist on target {}", propName, target.getClass()); + throw new RuntimeException(String.format("Property %s does not exist on target %s", propName, target.getClass())); + } + + try { + Class<?> paramClass = writeMethod.getParameterTypes()[0]; + if (paramClass == int.class) { + writeMethod.invoke(target, Integer.parseInt(propValue.toString())); + } + else if (paramClass == long.class) { + writeMethod.invoke(target, Long.parseLong(propValue.toString())); + } + else if (paramClass == boolean.class || paramClass == Boolean.class) { + writeMethod.invoke(target, Boolean.parseBoolean(propValue.toString())); + } + else if (paramClass == String.class) { + writeMethod.invoke(target, propValue.toString()); + } + else { + writeMethod.invoke(target, propValue); + } + } + catch (Exception e) { + LOGGER.error("Failed to set property {} on target {}", propName, target.getClass(), e); + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/com/zaxxer/hikari/util/QueuedSequenceSynchronizer.java b/src/main/java/com/zaxxer/hikari/util/QueuedSequenceSynchronizer.java new file mode 100644 index 0000000..eac7ec7 --- /dev/null +++ b/src/main/java/com/zaxxer/hikari/util/QueuedSequenceSynchronizer.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2015 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.util; + +import java.util.concurrent.atomic.LongAdder; +import java.util.concurrent.locks.AbstractQueuedLongSynchronizer; + +/** + * A specialized wait/notify class useful for resource tracking through the + * use of a monotonically-increasing long sequence. + * <p> + * When a shared resource becomes available the {@link #signal()} method should + * be called unconditionally. + * <p> + * A thread wishing to acquire a shared resource should: <br> + * <ul> + * <li>Obtain the current sequence from the {@link #currentSequence()} method </li> + * <li>Call {@link #waitUntilSequenceExceeded(long, long)} with that sequence. </li> + * <li>Upon receiving a <code>true</code> result from {@link #waitUntilSequenceExceeded(long, long)}, + * the current sequence should again be obtained from the {@link #currentSequence()} method, + * and an attempt to acquire the resource should be made. </li> + * <li>If the shared resource cannot be acquired, the thread should again call + * {@link #waitUntilSequenceExceeded(long, long)} with the previously obtained sequence. </li> + * <li>If <code>false</code> is received from {@link #waitUntilSequenceExceeded(long, long)} + * then a timeout has occurred. </li> + * </ul> + * <p> + * When running on Java 8 and above, this class leverages the fact that when {@link LongAdder} + * is monotonically increasing, and only {@link LongAdder#increment()} and {@link LongAdder#sum()} + * are used, it can be relied on to be Sequentially Consistent. + * + * @see <a href="http://en.wikipedia.org/wiki/Sequential_consistency">Java Spec</a> + * @author Brett Wooldridge + */ +public final class QueuedSequenceSynchronizer +{ + private final Sequence sequence; + private final Synchronizer synchronizer; + + /** + * Default constructor + */ + public QueuedSequenceSynchronizer() + { + this.synchronizer = new Synchronizer(); + this.sequence = Sequence.Factory.create(); + } + + /** + * Signal any waiting threads. + */ + public void signal() + { + synchronizer.releaseShared(1); + } + + /** + * Get the current sequence. + * + * @return the current sequence + */ + public long currentSequence() + { + return sequence.get(); + } + + /** + * Block the current thread until the current sequence exceeds the specified threshold, or + * until the specified timeout is reached. + * + * @param sequence the threshold the sequence must reach before this thread becomes unblocked + * @param nanosTimeout a nanosecond timeout specifying the maximum time to wait + * @return true if the threshold was reached, false if the wait timed out + * @throws InterruptedException if the thread is interrupted while waiting + */ + public boolean waitUntilSequenceExceeded(long sequence, long nanosTimeout) throws InterruptedException + { + return synchronizer.tryAcquireSharedNanos(sequence, nanosTimeout); + } + + /** + * Queries whether any threads are waiting to for the sequence to reach a particular threshold. + * + * @return true if there may be other threads waiting for a sequence threshold to be reached + */ + public boolean hasQueuedThreads() + { + return synchronizer.hasQueuedThreads(); + } + + /** + * Returns an estimate of the number of threads waiting for a sequence threshold to be reached. The + * value is only an estimate because the number of threads may change dynamically while this method + * traverses internal data structures. This method is designed for use in monitoring system state, + * not for synchronization control. + * + * @return the estimated number of threads waiting for a sequence threshold to be reached + */ + public int getQueueLength() + { + return synchronizer.getQueueLength(); + } + + private final class Synchronizer extends AbstractQueuedLongSynchronizer + { + private static final long serialVersionUID = 104753538004341218L; + + /** {@inheritDoc} */ + @Override + protected long tryAcquireShared(final long seq) + { + return sequence.get() - (seq + 1); + } + + /** {@inheritDoc} */ + @Override + protected boolean tryReleaseShared(final long unused) + { + sequence.increment(); + return true; + } + } +} diff --git a/src/main/java/com/zaxxer/hikari/util/Sequence.java b/src/main/java/com/zaxxer/hikari/util/Sequence.java new file mode 100644 index 0000000..b7abd3c --- /dev/null +++ b/src/main/java/com/zaxxer/hikari/util/Sequence.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2015 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.util; + +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.LongAdder; + +/** + * A monotonically increasing long sequence. + * + * @author brettw + */ +@SuppressWarnings("serial") +public interface Sequence +{ + /** + * Increments the current sequence by one. + */ + void increment(); + + /** + * Get the current sequence. + * + * @return the current sequence. + */ + long get(); + + /** + * Factory class used to create a platform-specific ClockSource. + */ + final class Factory + { + public static Sequence create() + { + if (!Boolean.getBoolean("com.zaxxer.hikari.useAtomicLongSequence")) { + return new Java8Sequence(); + } + else { + return new Java7Sequence(); + } + } + } + + final class Java7Sequence extends AtomicLong implements Sequence { + @Override + public void increment() { + this.incrementAndGet(); + } + } + + final class Java8Sequence extends LongAdder implements Sequence { + @Override + public long get() { + return this.sum(); + } + } +} diff --git a/src/main/java/com/zaxxer/hikari/util/SuspendResumeLock.java b/src/main/java/com/zaxxer/hikari/util/SuspendResumeLock.java new file mode 100644 index 0000000..0be8113 --- /dev/null +++ b/src/main/java/com/zaxxer/hikari/util/SuspendResumeLock.java @@ -0,0 +1,79 @@ +/* + * 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.util; + +import java.util.concurrent.Semaphore; + +/** + * This class implements a lock that can be used to suspend and resume the pool. It + * also provides a faux implementation that is used when the feature is disabled that + * hopefully gets fully "optimized away" by the JIT. + * + * @author Brett Wooldridge + */ +public class SuspendResumeLock +{ + public static final SuspendResumeLock FAUX_LOCK = new SuspendResumeLock(false) { + @Override + public void acquire() {} + + @Override + public void release() {} + + @Override + public void suspend() {} + + @Override + public void resume() {} + }; + + private static final int MAX_PERMITS = 10000; + private final Semaphore acquisitionSemaphore; + + /** + * Default constructor + */ + public SuspendResumeLock() + { + this(true); + } + + private SuspendResumeLock(final boolean createSemaphore) + { + acquisitionSemaphore = (createSemaphore ? new Semaphore(MAX_PERMITS, true) : null); + } + + public void acquire() + { + acquisitionSemaphore.acquireUninterruptibly(); + } + + public void release() + { + acquisitionSemaphore.release(); + } + + public void suspend() + { + acquisitionSemaphore.acquireUninterruptibly(MAX_PERMITS); + } + + public void resume() + { + acquisitionSemaphore.release(MAX_PERMITS); + } +} diff --git a/src/main/java/com/zaxxer/hikari/util/UtilityElf.java b/src/main/java/com/zaxxer/hikari/util/UtilityElf.java new file mode 100644 index 0000000..35b04d1 --- /dev/null +++ b/src/main/java/com/zaxxer/hikari/util/UtilityElf.java @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2013 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.util; + +import static java.lang.Thread.currentThread; +import static java.util.concurrent.TimeUnit.SECONDS; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.sql.Connection; +import java.util.Locale; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; + +/** + * + * @author Brett Wooldridge + */ +public final class UtilityElf +{ + /** + * + * @return null if string is null or empty + */ + public static String getNullIfEmpty(final String text) + { + return text == null ? null : text.trim().isEmpty() ? null : text.trim(); + } + + /** + * Sleep and suppress InterruptedException (but re-signal it). + * + * @param millis the number of milliseconds to sleep + */ + public static void quietlySleep(final long millis) + { + try { + Thread.sleep(millis); + } + catch (InterruptedException e) { + // I said be quiet! + currentThread().interrupt(); + } + } + + /** + * Create and instance of the specified class using the constructor matching the specified + * arguments. + * + * @param <T> the class type + * @param className the name of the class to instantiate + * @param clazz a class to cast the result as + * @param args arguments to a constructor + * @return an instance of the specified class + */ + public static <T> T createInstance(final String className, final Class<T> clazz, final Object... args) + { + if (className == null) { + return null; + } + + try { + Class<?> loaded = UtilityElf.class.getClassLoader().loadClass(className); + if (args.length == 0) { + return clazz.cast(loaded.newInstance()); + } + + Class<?>[] argClasses = new Class<?>[args.length]; + for (int i = 0; i < args.length; i++) { + argClasses[i] = args[i].getClass(); + } + Constructor<?> constructor = loaded.getConstructor(argClasses); + return clazz.cast(constructor.newInstance(args)); + } + catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * Create a ThreadPoolExecutor. + * + * @param queueSize the queue size + * @param threadName the thread name + * @param threadFactory an optional ThreadFactory + * @param policy the RejectedExecutionHandler policy + * @return a ThreadPoolExecutor + */ + public static ThreadPoolExecutor createThreadPoolExecutor(final int queueSize, final String threadName, ThreadFactory threadFactory, final RejectedExecutionHandler policy) + { + if (threadFactory == null) { + threadFactory = new DefaultThreadFactory(threadName, true); + } + + LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(queueSize); + ThreadPoolExecutor executor = new ThreadPoolExecutor(1 /*core*/, 1 /*max*/, 5 /*keepalive*/, SECONDS, queue, threadFactory, policy); + executor.allowCoreThreadTimeOut(true); + return executor; + } + + /** + * Create a ThreadPoolExecutor. + * + * @param queue the BlockingQueue to use + * @param threadName the thread name + * @param threadFactory an optional ThreadFactory + * @param policy the RejectedExecutionHandler policy + * @return a ThreadPoolExecutor + */ + public static ThreadPoolExecutor createThreadPoolExecutor(final BlockingQueue<Runnable> queue, final String threadName, ThreadFactory threadFactory, final RejectedExecutionHandler policy) + { + if (threadFactory == null) { + threadFactory = new DefaultThreadFactory(threadName, true); + } + + ThreadPoolExecutor executor = new ThreadPoolExecutor(1 /*core*/, 1 /*max*/, 5 /*keepalive*/, SECONDS, queue, threadFactory, policy); + executor.allowCoreThreadTimeOut(true); + return executor; + } + + // *********************************************************************** + // Misc. public methods + // *********************************************************************** + + /** + * 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 { + // use the english locale to avoid the infamous turkish locale bug + final String upperName = transactionIsolationName.toUpperCase(Locale.ENGLISH); + if (upperName.startsWith("TRANSACTION_")) { + Field field = Connection.class.getField(upperName); + return field.getInt(null); + } + final int level = Integer.parseInt(transactionIsolationName); + switch (level) { + case Connection.TRANSACTION_READ_UNCOMMITTED: + case Connection.TRANSACTION_READ_COMMITTED: + case Connection.TRANSACTION_REPEATABLE_READ: + case Connection.TRANSACTION_SERIALIZABLE: + case Connection.TRANSACTION_NONE: + return level; + default: + throw new IllegalArgumentException(); + } + } + catch (Exception e) { + throw new IllegalArgumentException("Invalid transaction isolation value: " + transactionIsolationName); + } + } + + return -1; + } + + public static final class DefaultThreadFactory implements ThreadFactory { + + private final String threadName; + private final boolean daemon; + + public DefaultThreadFactory(String threadName, boolean daemon) { + this.threadName = threadName; + this.daemon = daemon; + } + + @Override + public Thread newThread(Runnable r) { + Thread thread = new Thread(r, threadName); + thread.setDaemon(daemon); + return thread; + } + } +} |