diff options
author | Apollon Oikonomopoulos <apoikos@debian.org> | 2017-03-01 14:21:59 +0200 |
---|---|---|
committer | Apollon Oikonomopoulos <apoikos@debian.org> | 2017-03-01 14:21:59 +0200 |
commit | bd7b6679cea5620446718911de7a6764f81a9a7a (patch) | |
tree | a725b29afd153ae0d18c47e41c1197e2b409313d /src |
New upstream version 2.6.0
Diffstat (limited to 'src')
84 files changed, 16265 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; + } + } +} diff --git a/src/test/java/com/zaxxer/hikari/db/BasicPoolTest.java b/src/test/java/com/zaxxer/hikari/db/BasicPoolTest.java new file mode 100644 index 0000000..c9040b0 --- /dev/null +++ b/src/test/java/com/zaxxer/hikari/db/BasicPoolTest.java @@ -0,0 +1,149 @@ +/* + * 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.db; + +import static com.zaxxer.hikari.pool.TestElf.newHikariConfig; +import static com.zaxxer.hikari.pool.TestElf.getPool; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import com.zaxxer.hikari.pool.HikariPool; + +/** + * @author brettw + * + */ +public class BasicPoolTest +{ + @Before + public void setup() throws SQLException + { + HikariConfig config = newHikariConfig(); + config.setMinimumIdle(1); + config.setMaximumPoolSize(2); + config.setConnectionTestQuery("SELECT 1"); + config.setDataSourceClassName("org.h2.jdbcx.JdbcDataSource"); + config.addDataSourceProperty("url", "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1"); + + try (HikariDataSource ds = new HikariDataSource(config); + Connection conn = ds.getConnection(); + Statement stmt = conn.createStatement()) { + stmt.executeUpdate("DROP TABLE IF EXISTS basic_pool_test"); + stmt.executeUpdate("CREATE TABLE basic_pool_test (" + + "id INTEGER NOT NULL IDENTITY PRIMARY KEY, " + + "timestamp TIMESTAMP, " + + "string VARCHAR(128), " + + "string_from_number NUMERIC " + + ")"); + } + } + + @Test + public void testIdleTimeout() throws InterruptedException, SQLException + { + HikariConfig config = newHikariConfig(); + config.setMinimumIdle(5); + config.setMaximumPoolSize(10); + config.setConnectionTestQuery("SELECT 1"); + config.setDataSourceClassName("org.h2.jdbcx.JdbcDataSource"); + config.addDataSourceProperty("url", "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1"); + + System.setProperty("com.zaxxer.hikari.housekeeping.periodMs", "1000"); + + try (HikariDataSource ds = new HikariDataSource(config)) { + System.clearProperty("com.zaxxer.hikari.housekeeping.periodMs"); + + SECONDS.sleep(1); + + HikariPool pool = getPool(ds); + + ds.setIdleTimeout(3000); + + assertEquals("Total connections not as expected", 5, pool.getTotalConnections()); + assertEquals("Idle connections not as expected", 5, pool.getIdleConnections()); + + try (Connection connection = ds.getConnection()) { + Assert.assertNotNull(connection); + + MILLISECONDS.sleep(1500); + + assertEquals("Second total connections not as expected", 6, pool.getTotalConnections()); + assertEquals("Second idle connections not as expected", 5, pool.getIdleConnections()); + } + + assertEquals("Idle connections not as expected", 6, pool.getIdleConnections()); + + SECONDS.sleep(2); + + assertEquals("Third total connections not as expected", 5, pool.getTotalConnections()); + assertEquals("Third idle connections not as expected", 5, pool.getIdleConnections()); + } + } + + @Test + public void testIdleTimeout2() throws InterruptedException, SQLException + { + HikariConfig config = newHikariConfig(); + config.setMaximumPoolSize(50); + config.setConnectionTestQuery("SELECT 1"); + config.setDataSourceClassName("org.h2.jdbcx.JdbcDataSource"); + config.addDataSourceProperty("url", "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1"); + + System.setProperty("com.zaxxer.hikari.housekeeping.periodMs", "1000"); + + try (HikariDataSource ds = new HikariDataSource(config)) { + System.clearProperty("com.zaxxer.hikari.housekeeping.periodMs"); + + SECONDS.sleep(1); + + HikariPool pool = getPool(ds); + + ds.setIdleTimeout(3000); + + assertEquals("Total connections not as expected", 50, pool.getTotalConnections()); + assertEquals("Idle connections not as expected", 50, pool.getIdleConnections()); + + try (Connection connection = ds.getConnection()) { + assertNotNull(connection); + + MILLISECONDS.sleep(1500); + + assertEquals("Second total connections not as expected", 50, pool.getTotalConnections()); + assertEquals("Second idle connections not as expected", 49, pool.getIdleConnections()); + } + + assertEquals("Idle connections not as expected", 50, pool.getIdleConnections()); + + SECONDS.sleep(3); + + assertEquals("Third total connections not as expected", 50, pool.getTotalConnections()); + assertEquals("Third idle connections not as expected", 50, pool.getIdleConnections()); + } + } +} diff --git a/src/test/java/com/zaxxer/hikari/metrics/dropwizard/CodaHaleMetricsTrackerTest.java b/src/test/java/com/zaxxer/hikari/metrics/dropwizard/CodaHaleMetricsTrackerTest.java new file mode 100755 index 0000000..8e2164c --- /dev/null +++ b/src/test/java/com/zaxxer/hikari/metrics/dropwizard/CodaHaleMetricsTrackerTest.java @@ -0,0 +1,39 @@ +package com.zaxxer.hikari.metrics.dropwizard; + +import static org.mockito.Mockito.verify; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import com.codahale.metrics.MetricRegistry; + +@RunWith(MockitoJUnitRunner.class) +public class CodaHaleMetricsTrackerTest { + + @Mock + public MetricRegistry mockMetricRegistry; + + private CodaHaleMetricsTracker testee; + + @Before + public void setup(){ + testee = new CodaHaleMetricsTracker("mypool", null, mockMetricRegistry); + } + + @Test + public void close() throws Exception { + testee.close(); + + verify(mockMetricRegistry).remove("mypool.pool.Wait"); + verify(mockMetricRegistry).remove("mypool.pool.Usage"); + verify(mockMetricRegistry).remove("mypool.pool.ConnectionCreation"); + verify(mockMetricRegistry).remove("mypool.pool.ConnectionTimeoutRate"); + verify(mockMetricRegistry).remove("mypool.pool.TotalConnections"); + verify(mockMetricRegistry).remove("mypool.pool.IdleConnections"); + verify(mockMetricRegistry).remove("mypool.pool.ActiveConnections"); + verify(mockMetricRegistry).remove("mypool.pool.PendingConnections"); + } +} diff --git a/src/test/java/com/zaxxer/hikari/metrics/prometheus/HikariCPCollectorTest.java b/src/test/java/com/zaxxer/hikari/metrics/prometheus/HikariCPCollectorTest.java new file mode 100644 index 0000000..46b2e74 --- /dev/null +++ b/src/test/java/com/zaxxer/hikari/metrics/prometheus/HikariCPCollectorTest.java @@ -0,0 +1,125 @@ +/* + * 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 static com.zaxxer.hikari.pool.TestElf.newHikariConfig; +import static com.zaxxer.hikari.util.UtilityElf.quietlySleep; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +import java.sql.Connection; + +import org.junit.Test; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import com.zaxxer.hikari.mocks.StubConnection; + +import io.prometheus.client.CollectorRegistry; + +public class HikariCPCollectorTest { + @Test + public void noConnection() throws Exception { + HikariConfig config = newHikariConfig(); + config.setMinimumIdle(0); + config.setMetricsTrackerFactory(new PrometheusMetricsTrackerFactory()); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + StubConnection.slowCreate = true; + try (HikariDataSource ds = new HikariDataSource(config)) { + assertThat(getValue("hikaricp_active_connections", "noConnection"), is(0.0)); + assertThat(getValue("hikaricp_idle_connections", "noConnection"), is(0.0)); + assertThat(getValue("hikaricp_pending_threads", "noConnection"), is(0.0)); + assertThat(getValue("hikaricp_connections", "noConnection"), is(0.0)); + } + finally { + StubConnection.slowCreate = false; + } + } + + @Test + public void noConnectionWithoutPoolName() throws Exception { + HikariConfig config = new HikariConfig(); + config.setMinimumIdle(0); + config.setMetricsTrackerFactory(new PrometheusMetricsTrackerFactory()); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + StubConnection.slowCreate = true; + try (HikariDataSource ds = new HikariDataSource(config)) { + assertThat(getValue("hikaricp_active_connections", "HikariPool-1"), is(0.0)); + assertThat(getValue("hikaricp_idle_connections", "HikariPool-1"), is(0.0)); + assertThat(getValue("hikaricp_pending_threads", "HikariPool-1"), is(0.0)); + assertThat(getValue("hikaricp_connections", "HikariPool-1"), is(0.0)); + } + finally { + StubConnection.slowCreate = false; + } + } + + @Test + public void connection1() throws Exception { + HikariConfig config = newHikariConfig(); + config.setMetricsTrackerFactory(new PrometheusMetricsTrackerFactory()); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + config.setMaximumPoolSize(1); + + StubConnection.slowCreate = true; + try (HikariDataSource ds = new HikariDataSource(config); + Connection connection1 = ds.getConnection()) { + + quietlySleep(1000); + + assertThat(getValue("hikaricp_active_connections", "connection1"), is(1.0)); + assertThat(getValue("hikaricp_idle_connections", "connection1"), is(0.0)); + assertThat(getValue("hikaricp_pending_threads", "connection1"), is(0.0)); + assertThat(getValue("hikaricp_connections", "connection1"), is(1.0)); + } + finally { + StubConnection.slowCreate = false; + } + } + + @Test + public void connectionClosed() throws Exception { + HikariConfig config = newHikariConfig(); + config.setMetricsTrackerFactory(new PrometheusMetricsTrackerFactory()); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + config.setMaximumPoolSize(1); + + StubConnection.slowCreate = true; + try (HikariDataSource ds = new HikariDataSource(config)) { + try (Connection connection1 = ds.getConnection()) { + // close immediately + } + + assertThat(getValue("hikaricp_active_connections", "connectionClosed"), is(0.0)); + assertThat(getValue("hikaricp_idle_connections", "connectionClosed"), is(1.0)); + assertThat(getValue("hikaricp_pending_threads", "connectionClosed"), is(0.0)); + assertThat(getValue("hikaricp_connections", "connectionClosed"), is(1.0)); + } + finally { + StubConnection.slowCreate = false; + } + } + + private double getValue(String name, String poolName) { + String[] labelNames = {"pool"}; + String[] labelValues = {poolName}; + return CollectorRegistry.defaultRegistry.getSampleValue(name, labelNames, labelValues); + } + +} diff --git a/src/test/java/com/zaxxer/hikari/metrics/prometheus/PrometheusMetricsTrackerTest.java b/src/test/java/com/zaxxer/hikari/metrics/prometheus/PrometheusMetricsTrackerTest.java new file mode 100644 index 0000000..a7b0b03 --- /dev/null +++ b/src/test/java/com/zaxxer/hikari/metrics/prometheus/PrometheusMetricsTrackerTest.java @@ -0,0 +1,78 @@ +/* + * 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 static com.zaxxer.hikari.pool.TestElf.newHikariConfig; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +import java.sql.Connection; +import java.sql.SQLTransientConnectionException; + +import org.junit.Test; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; + +import io.prometheus.client.CollectorRegistry; + +public class PrometheusMetricsTrackerTest { + @Test + public void recordConnectionTimeout() throws Exception { + HikariConfig config = newHikariConfig(); + config.setMetricsTrackerFactory(new PrometheusMetricsTrackerFactory()); + config.setJdbcUrl("jdbc:h2:mem:"); + config.setMaximumPoolSize(2); + config.setConnectionTimeout(250); + + String[] labelNames = {"pool"}; + String[] labelValues = {config.getPoolName()}; + + try (HikariDataSource hikariDataSource = new HikariDataSource(config)) { + try (Connection connection1 = hikariDataSource.getConnection(); + Connection connection2 = hikariDataSource.getConnection()) { + try (Connection connection3 = hikariDataSource.getConnection()) { + } + catch (SQLTransientConnectionException ignored) { + } + } + + assertThat(CollectorRegistry.defaultRegistry.getSampleValue( + "hikaricp_connection_timeout_count", + labelNames, + labelValues), is(1.0)); + assertThat(CollectorRegistry.defaultRegistry.getSampleValue( + "hikaricp_connection_acquired_nanos_count", + labelNames, + labelValues), is(equalTo(2.0))); + assertTrue(CollectorRegistry.defaultRegistry.getSampleValue( + "hikaricp_connection_acquired_nanos_sum", + labelNames, + labelValues) > 0.0); + assertThat(CollectorRegistry.defaultRegistry.getSampleValue( + "hikaricp_connection_usage_millis_count", + labelNames, + labelValues), is(equalTo(2.0))); + assertTrue(CollectorRegistry.defaultRegistry.getSampleValue( + "hikaricp_connection_usage_millis_sum", + labelNames, + labelValues) > 0.0); + } + } +} diff --git a/src/test/java/com/zaxxer/hikari/mocks/MockDataSource.java b/src/test/java/com/zaxxer/hikari/mocks/MockDataSource.java new file mode 100644 index 0000000..558f9fb --- /dev/null +++ b/src/test/java/com/zaxxer/hikari/mocks/MockDataSource.java @@ -0,0 +1,162 @@ +/* + * 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.mocks; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.PrintWriter; +import java.sql.CallableStatement; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.sql.Statement; +import java.util.logging.Logger; + +import javax.sql.DataSource; + +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +/** + * + * @author Brett Wooldridge + */ +public class MockDataSource implements DataSource +{ + @Override + public Connection getConnection() throws SQLException + { + return createMockConnection(); + } + + @Override + public Connection getConnection(String username, String password) throws SQLException + { + return getConnection(); + } + + @Override + public PrintWriter getLogWriter() throws SQLException + { + return null; + } + + @Override + public void setLogWriter(PrintWriter out) throws SQLException + { + } + + @Override + public void setLoginTimeout(int seconds) throws SQLException + { + } + + @Override + public int getLoginTimeout() throws SQLException + { + return 0; + } + + public Logger getParentLogger() throws SQLFeatureNotSupportedException + { + return null; + } + + @Override + public <T> T unwrap(Class<T> iface) throws SQLException + { + return null; + } + + @Override + public boolean isWrapperFor(Class<?> iface) throws SQLException + { + return false; + } + + public static Connection createMockConnection() throws SQLException { + // Setup mock connection + final Connection mockConnection = mock(Connection.class); + + // Autocommit is always true by default + when(mockConnection.getAutoCommit()).thenReturn(true); + + // Handle Connection.createStatement() + Statement statement = mock(Statement.class); + when(mockConnection.createStatement()).thenReturn(statement); + when(mockConnection.createStatement(anyInt(), anyInt())).thenReturn(statement); + when(mockConnection.createStatement(anyInt(), anyInt(), anyInt())).thenReturn(statement); + when(mockConnection.isValid(anyInt())).thenReturn(true); + + // Handle Connection.prepareStatement() + PreparedStatement mockPreparedStatement = mock(PreparedStatement.class); + when(mockConnection.prepareStatement(anyString())).thenReturn(mockPreparedStatement); + when(mockConnection.prepareStatement(anyString(), anyInt())).thenReturn(mockPreparedStatement); + when(mockConnection.prepareStatement(anyString(), any(int[].class))).thenReturn(mockPreparedStatement); + when(mockConnection.prepareStatement(anyString(), any(String[].class))).thenReturn(mockPreparedStatement); + when(mockConnection.prepareStatement(anyString(), anyInt(), anyInt())).thenReturn(mockPreparedStatement); + when(mockConnection.prepareStatement(anyString(), anyInt(), anyInt(), anyInt())).thenReturn(mockPreparedStatement); + doAnswer(new Answer<Void>() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable + { + return null; + } + }).doNothing().when(mockPreparedStatement).setInt(anyInt(), anyInt()); + + ResultSet mockResultSet = mock(ResultSet.class); + when(mockPreparedStatement.executeQuery()).thenReturn(mockResultSet); + when(mockResultSet.getString(anyInt())).thenReturn("aString"); + when(mockResultSet.next()).thenReturn(true); + + // Handle Connection.prepareCall() + CallableStatement mockCallableStatement = mock(CallableStatement.class); + when(mockConnection.prepareCall(anyString())).thenReturn(mockCallableStatement); + when(mockConnection.prepareCall(anyString(), anyInt(), anyInt())).thenReturn(mockCallableStatement); + when(mockConnection.prepareCall(anyString(), anyInt(), anyInt(), anyInt())).thenReturn(mockCallableStatement); + + // Handle Connection.close() +// doAnswer(new Answer<Void>() { +// public Void answer(InvocationOnMock invocation) throws Throwable { +// return null; +// } +// }).doThrow(new SQLException("Connection is already closed")).when(mockConnection).close(); + + // Handle Connection.commit() +// doAnswer(new Answer<Void>() { +// public Void answer(InvocationOnMock invocation) throws Throwable { +// return null; +// } +// }).doThrow(new SQLException("Transaction already commited")).when(mockConnection).commit(); + + // Handle Connection.rollback() +// doAnswer(new Answer<Void>() { +// public Void answer(InvocationOnMock invocation) throws Throwable { +// return null; +// } +// }).doThrow(new SQLException("Transaction already rolledback")).when(mockConnection).rollback(); + + return mockConnection; + } +} diff --git a/src/test/java/com/zaxxer/hikari/mocks/StubBaseConnection.java b/src/test/java/com/zaxxer/hikari/mocks/StubBaseConnection.java new file mode 100644 index 0000000..f88705e --- /dev/null +++ b/src/test/java/com/zaxxer/hikari/mocks/StubBaseConnection.java @@ -0,0 +1,47 @@ +/* + * 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.mocks; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Statement; + +public abstract class StubBaseConnection implements Connection +{ + public volatile boolean throwException; + + /** {@inheritDoc} */ + @Override + public Statement createStatement() throws SQLException + { + if (throwException) { + throw new SQLException(); + } + return new StubStatement(this); + } + + /** {@inheritDoc} */ + @Override + public PreparedStatement prepareStatement(String sql) throws SQLException + { + if (throwException) { + throw new SQLException(); + } + return new StubPreparedStatement(this); + } +} diff --git a/src/test/java/com/zaxxer/hikari/mocks/StubConnection.java b/src/test/java/com/zaxxer/hikari/mocks/StubConnection.java new file mode 100644 index 0000000..f96624b --- /dev/null +++ b/src/test/java/com/zaxxer/hikari/mocks/StubConnection.java @@ -0,0 +1,486 @@ +/* + * 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.mocks; + +import java.sql.Array; +import java.sql.Blob; +import java.sql.CallableStatement; +import java.sql.Clob; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.NClob; +import java.sql.PreparedStatement; +import java.sql.SQLClientInfoException; +import java.sql.SQLException; +import java.sql.SQLWarning; +import java.sql.SQLXML; +import java.sql.Savepoint; +import java.sql.Statement; +import java.sql.Struct; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicInteger; + +import com.zaxxer.hikari.util.UtilityElf; + +/** + * + * @author Brett Wooldridge + */ +public class StubConnection extends StubBaseConnection implements Connection +{ + public static final AtomicInteger count = new AtomicInteger(); + public static volatile boolean slowCreate; + public static volatile boolean oldDriver; + + private static long foo; + private boolean autoCommit; + private int isolation = Connection.TRANSACTION_READ_COMMITTED; + private String catalog; + + static { + foo = System.currentTimeMillis(); + } + + public StubConnection() { + count.incrementAndGet(); + if (slowCreate) { + UtilityElf.quietlySleep(1000); + } + } + + /** {@inheritDoc} */ + @SuppressWarnings("unchecked") + @Override + public <T> T unwrap(Class<T> iface) throws SQLException + { + if (throwException) { + throw new SQLException(); + } + + if (iface.isInstance(this)) { + return (T) this; + } + + throw new SQLException("Wrapped connection is not an instance of " + iface); + } + + /** {@inheritDoc} */ + @Override + public boolean isWrapperFor(Class<?> iface) throws SQLException + { + if (throwException) { + throw new SQLException(); + } + return false; + } + + /** {@inheritDoc} */ + @Override + public CallableStatement prepareCall(String sql) throws SQLException + { + if (throwException) { + throw new SQLException(); + } + return null; + } + + /** {@inheritDoc} */ + @Override + public String nativeSQL(String sql) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public void setAutoCommit(boolean autoCommit) throws SQLException + { + if (throwException) { + throw new SQLException(); + } + this.autoCommit = autoCommit; + } + + /** {@inheritDoc} */ + @Override + public boolean getAutoCommit() throws SQLException + { + return autoCommit; + } + + /** {@inheritDoc} */ + @Override + public void commit() throws SQLException + { + + } + + /** {@inheritDoc} */ + @Override + public void rollback() throws SQLException + { + + } + + /** {@inheritDoc} */ + @Override + public void close() throws SQLException + { + + } + + /** {@inheritDoc} */ + @Override + public boolean isClosed() throws SQLException + { + if (throwException) { + throw new SQLException(); + } + return false; + } + + /** {@inheritDoc} */ + @Override + public DatabaseMetaData getMetaData() throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public void setReadOnly(boolean readOnly) throws SQLException + { + if (throwException) { + throw new SQLException(); + } + } + + /** {@inheritDoc} */ + @Override + public boolean isReadOnly() throws SQLException + { + if (throwException) { + throw new SQLException(); + } + return false; + } + + /** {@inheritDoc} */ + @Override + public void setCatalog(String catalog) throws SQLException + { + if (throwException) { + throw new SQLException(); + } + this.catalog = catalog; + } + + /** {@inheritDoc} */ + @Override + public String getCatalog() throws SQLException + { + return catalog; + } + + /** {@inheritDoc} */ + @Override + public void setTransactionIsolation(int level) throws SQLException + { + if (throwException) { + throw new SQLException(); + } + this.isolation = level; + } + + /** {@inheritDoc} */ + @Override + public int getTransactionIsolation() throws SQLException + { + return isolation; + } + + /** {@inheritDoc} */ + @Override + public SQLWarning getWarnings() throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public void clearWarnings() throws SQLException + { + if (throwException) { + throw new SQLException(); + } + } + + /** {@inheritDoc} */ + @Override + public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException + { + if (throwException) { + throw new SQLException(); + } + return null; + } + + /** {@inheritDoc} */ + @Override + public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException + { + if (throwException) { + throw new SQLException(); + } + return new StubPreparedStatement(this); + } + + /** {@inheritDoc} */ + @Override + public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException + { + if (throwException) { + throw new SQLException(); + } + return null; + } + + /** {@inheritDoc} */ + @Override + public Map<String, Class<?>> getTypeMap() throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public void setTypeMap(Map<String, Class<?>> map) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void setHoldability(int holdability) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public int getHoldability() throws SQLException + { + return (int) foo; + } + + /** {@inheritDoc} */ + @Override + public Savepoint setSavepoint() throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public Savepoint setSavepoint(String name) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public void rollback(Savepoint savepoint) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void releaseSavepoint(Savepoint savepoint) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException + { + if (throwException) { + throw new SQLException(); + } + return null; + } + + /** {@inheritDoc} */ + @Override + public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException + { + if (throwException) { + throw new SQLException(); + } + return new StubPreparedStatement(this); + } + + /** {@inheritDoc} */ + @Override + public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException + { + if (throwException) { + throw new SQLException(); + } + return null; + } + + /** {@inheritDoc} */ + @Override + public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException + { + if (throwException) { + throw new SQLException(); + } + return new StubPreparedStatement(this); + } + + /** {@inheritDoc} */ + @Override + public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException + { + if (throwException) { + throw new SQLException(); + } + return new StubPreparedStatement(this); + } + + /** {@inheritDoc} */ + @Override + public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException + { + if (throwException) { + throw new SQLException(); + } + return new StubPreparedStatement(this); + } + + /** {@inheritDoc} */ + @Override + public Clob createClob() throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public Blob createBlob() throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public NClob createNClob() throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public SQLXML createSQLXML() throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public boolean isValid(int timeout) throws SQLException + { + if (throwException) { + throw new SQLException(); + } + return true; + } + + /** {@inheritDoc} */ + @Override + public void setClientInfo(String name, String value) throws SQLClientInfoException + { + } + + /** {@inheritDoc} */ + @Override + public void setClientInfo(Properties properties) throws SQLClientInfoException + { + } + + /** {@inheritDoc} */ + @Override + public String getClientInfo(String name) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public Properties getClientInfo() throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public Array createArrayOf(String typeName, Object[] elements) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public Struct createStruct(String typeName, Object[] attributes) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + public void setSchema(String schema) throws SQLException + { + } + + /** {@inheritDoc} */ + public String getSchema() throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + public void abort(Executor executor) throws SQLException + { + throw new SQLException("Intentional exception during abort"); + } + + /** {@inheritDoc} */ + public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException + { + } + + /** {@inheritDoc} */ + public int getNetworkTimeout() throws SQLException + { + if (oldDriver) { + throw new AbstractMethodError(); + } + + return 0; + } + +} diff --git a/src/test/java/com/zaxxer/hikari/mocks/StubDataSource.java b/src/test/java/com/zaxxer/hikari/mocks/StubDataSource.java new file mode 100755 index 0000000..c7da189 --- /dev/null +++ b/src/test/java/com/zaxxer/hikari/mocks/StubDataSource.java @@ -0,0 +1,149 @@ +/* + * 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.mocks; + +import com.zaxxer.hikari.util.UtilityElf; + +import java.io.PrintWriter; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.util.logging.Logger; + +import javax.sql.DataSource; + +/** + * + * @author Brett Wooldridge + */ +public class StubDataSource implements DataSource +{ + private String user; + private String password; + private PrintWriter logWriter; + private SQLException throwException; + private long connectionAcquistionTime = 0; + private int loginTimeout; + + public String getUser() + { + return user; + } + + public void setUser(String user) + { + this.user = user; + } + + public String getPassword() + { + return password; + } + + public void setPassword(String password) + { + this.password = password; + } + + public void setURL(String url) + { + // we don't care + } + + /** {@inheritDoc} */ + @Override + public PrintWriter getLogWriter() throws SQLException + { + return logWriter; + } + + /** {@inheritDoc} */ + @Override + public void setLogWriter(PrintWriter out) throws SQLException + { + this.logWriter = out; + } + + /** {@inheritDoc} */ + @Override + public void setLoginTimeout(int seconds) throws SQLException + { + this.loginTimeout = seconds; + } + + /** {@inheritDoc} */ + @Override + public int getLoginTimeout() throws SQLException + { + return loginTimeout; + } + + /** {@inheritDoc} */ + public Logger getParentLogger() throws SQLFeatureNotSupportedException + { + return null; + } + + /** {@inheritDoc} */ + @SuppressWarnings("unchecked") + @Override + public <T> T unwrap(Class<T> iface) throws SQLException + { + if (iface.isInstance(this)) { + return (T) this; + } + + throw new SQLException("Wrapped DataSource is not an instance of " + iface); + } + + /** {@inheritDoc} */ + @Override + public boolean isWrapperFor(Class<?> iface) throws SQLException + { + return false; + } + + /** {@inheritDoc} */ + @Override + public Connection getConnection() throws SQLException + { + if (throwException != null) { + throw throwException; + } + if (connectionAcquistionTime > 0) { + UtilityElf.quietlySleep(connectionAcquistionTime); + } + + return new StubConnection(); + } + + /** {@inheritDoc} */ + @Override + public Connection getConnection(String username, String password) throws SQLException + { + return new StubConnection(); + } + + public void setThrowException(SQLException e) + { + this.throwException = e; + } + + public void setConnectionAcquistionTime(long connectionAcquisitionTime) { + this.connectionAcquistionTime = connectionAcquisitionTime; + } +} diff --git a/src/test/java/com/zaxxer/hikari/mocks/StubDriver.java b/src/test/java/com/zaxxer/hikari/mocks/StubDriver.java new file mode 100644 index 0000000..eff7292 --- /dev/null +++ b/src/test/java/com/zaxxer/hikari/mocks/StubDriver.java @@ -0,0 +1,96 @@ +/* + * 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.mocks; + +import java.sql.Connection; +import java.sql.Driver; +import java.sql.DriverManager; +import java.sql.DriverPropertyInfo; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.util.Properties; +import java.util.logging.Logger; + +/** + * + * @author Brett Wooldridge + */ +public class StubDriver implements Driver +{ + private static final Driver driver; + + static + { + driver = new StubDriver(); + try + { + DriverManager.registerDriver(driver); + } + catch (SQLException e) + { + e.printStackTrace(); + } + } + + /** {@inheritDoc} */ + @Override + public Connection connect(String url, Properties info) throws SQLException + { + return new StubConnection(); + } + + /** {@inheritDoc} */ + @Override + public boolean acceptsURL(String url) throws SQLException + { + return "jdbc:stub".equals(url); + } + + /** {@inheritDoc} */ + @Override + public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public int getMajorVersion() + { + return 0; + } + + /** {@inheritDoc} */ + @Override + public int getMinorVersion() + { + return 0; + } + + /** {@inheritDoc} */ + @Override + public boolean jdbcCompliant() + { + return true; + } + + /** {@inheritDoc} */ + public Logger getParentLogger() throws SQLFeatureNotSupportedException + { + return null; + } +} diff --git a/src/test/java/com/zaxxer/hikari/mocks/StubPreparedStatement.java b/src/test/java/com/zaxxer/hikari/mocks/StubPreparedStatement.java new file mode 100644 index 0000000..5c48f85 --- /dev/null +++ b/src/test/java/com/zaxxer/hikari/mocks/StubPreparedStatement.java @@ -0,0 +1,671 @@ +/* + * 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.mocks; + +import java.io.InputStream; +import java.io.Reader; +import java.math.BigDecimal; +import java.net.URL; +import java.sql.Array; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.Connection; +import java.sql.Date; +import java.sql.NClob; +import java.sql.ParameterMetaData; +import java.sql.PreparedStatement; +import java.sql.Ref; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.RowId; +import java.sql.SQLException; +import java.sql.SQLWarning; +import java.sql.SQLXML; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.Calendar; + +/** + * + * @author Brett Wooldridge + */ +public class StubPreparedStatement extends StubStatement implements PreparedStatement +{ + public StubPreparedStatement(Connection connection) + { + super(connection); + } + + /** {@inheritDoc} */ + @Override + public ResultSet executeQuery(String sql) throws SQLException + { + return new StubResultSet(); + } + + /** {@inheritDoc} */ + @Override + public int executeUpdate(String sql) throws SQLException + { + return 0; + } + + /** {@inheritDoc} */ + @Override + public int getMaxFieldSize() throws SQLException + { + throw new SQLException("Simulated disconnection error", "08999"); + } + + /** {@inheritDoc} */ + @Override + public void setMaxFieldSize(int max) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public int getMaxRows() throws SQLException + { + return 0; + } + + /** {@inheritDoc} */ + @Override + public void setMaxRows(int max) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void setEscapeProcessing(boolean enable) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public int getQueryTimeout() throws SQLException + { + return 0; + } + + /** {@inheritDoc} */ + @Override + public void setQueryTimeout(int seconds) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void cancel() throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public SQLWarning getWarnings() throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public void clearWarnings() throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void setCursorName(String name) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public boolean execute(String sql) throws SQLException + { + return false; + } + + /** {@inheritDoc} */ + @Override + public ResultSet getResultSet() throws SQLException + { + return new StubResultSet(); + } + + /** {@inheritDoc} */ + @Override + public int getUpdateCount() throws SQLException + { + return 0; + } + + /** {@inheritDoc} */ + @Override + public boolean getMoreResults() throws SQLException + { + if (isClosed()) + { + throw new SQLException("Connection is closed"); + } + return false; + } + + /** {@inheritDoc} */ + @Override + public void setFetchDirection(int direction) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public int getFetchDirection() throws SQLException + { + return 0; + } + + /** {@inheritDoc} */ + @Override + public void setFetchSize(int rows) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public int getFetchSize() throws SQLException + { + return 0; + } + + /** {@inheritDoc} */ + @Override + public int getResultSetConcurrency() throws SQLException + { + return 0; + } + + /** {@inheritDoc} */ + @Override + public int getResultSetType() throws SQLException + { + return 0; + } + + /** {@inheritDoc} */ + @Override + public void addBatch(String sql) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void clearBatch() throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public int[] executeBatch() throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public boolean getMoreResults(int current) throws SQLException + { + return false; + } + + /** {@inheritDoc} */ + @Override + public ResultSet getGeneratedKeys() throws SQLException + { + return new StubResultSet(); + } + + /** {@inheritDoc} */ + @Override + public int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException + { + return 0; + } + + /** {@inheritDoc} */ + @Override + public int executeUpdate(String sql, int[] columnIndexes) throws SQLException + { + return 0; + } + + /** {@inheritDoc} */ + @Override + public int executeUpdate(String sql, String[] columnNames) throws SQLException + { + return 0; + } + + /** {@inheritDoc} */ + @Override + public boolean execute(String sql, int autoGeneratedKeys) throws SQLException + { + return false; + } + + /** {@inheritDoc} */ + @Override + public boolean execute(String sql, int[] columnIndexes) throws SQLException + { + return false; + } + + /** {@inheritDoc} */ + @Override + public boolean execute(String sql, String[] columnNames) throws SQLException + { + return false; + } + + /** {@inheritDoc} */ + @Override + public int getResultSetHoldability() throws SQLException + { + return 0; + } + + /** {@inheritDoc} */ + @Override + public void setPoolable(boolean poolable) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public boolean isPoolable() throws SQLException + { + return false; + } + + /** {@inheritDoc} */ + @Override + public void closeOnCompletion() throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public boolean isCloseOnCompletion() throws SQLException + { + return false; + } + + /** {@inheritDoc} */ + @SuppressWarnings("unchecked") + @Override + public <T> T unwrap(Class<T> iface) throws SQLException + { + if (iface.isInstance(this)) { + return (T) this; + } + + throw new SQLException("Wrapped connection is not an instance of " + iface); + } + + /** {@inheritDoc} */ + @Override + public boolean isWrapperFor(Class<?> iface) throws SQLException + { + return false; + } + + /** {@inheritDoc} */ + @Override + public ResultSet executeQuery() throws SQLException + { + return new StubResultSet(); + } + + /** {@inheritDoc} */ + @Override + public int executeUpdate() throws SQLException + { + return 0; + } + + /** {@inheritDoc} */ + @Override + public void setNull(int parameterIndex, int sqlType) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void setBoolean(int parameterIndex, boolean x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void setByte(int parameterIndex, byte x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void setShort(int parameterIndex, short x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void setInt(int parameterIndex, int x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void setLong(int parameterIndex, long x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void setFloat(int parameterIndex, float x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void setDouble(int parameterIndex, double x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void setBigDecimal(int parameterIndex, BigDecimal x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void setString(int parameterIndex, String x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void setBytes(int parameterIndex, byte[] x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void setDate(int parameterIndex, Date x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void setTime(int parameterIndex, Time x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void setTimestamp(int parameterIndex, Timestamp x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void setAsciiStream(int parameterIndex, InputStream x, int length) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void setUnicodeStream(int parameterIndex, InputStream x, int length) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void setBinaryStream(int parameterIndex, InputStream x, int length) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void clearParameters() throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void setObject(int parameterIndex, Object x, int targetSqlType) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void setObject(int parameterIndex, Object x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public boolean execute() throws SQLException + { + return false; + } + + /** {@inheritDoc} */ + @Override + public void addBatch() throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void setCharacterStream(int parameterIndex, Reader reader, int length) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void setRef(int parameterIndex, Ref x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void setBlob(int parameterIndex, Blob x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void setClob(int parameterIndex, Clob x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void setArray(int parameterIndex, Array x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public ResultSetMetaData getMetaData() throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public void setDate(int parameterIndex, Date x, Calendar cal) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void setTime(int parameterIndex, Time x, Calendar cal) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void setNull(int parameterIndex, int sqlType, String typeName) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void setURL(int parameterIndex, URL x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public ParameterMetaData getParameterMetaData() throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public void setRowId(int parameterIndex, RowId x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void setNString(int parameterIndex, String value) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void setNCharacterStream(int parameterIndex, Reader value, long length) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void setNClob(int parameterIndex, NClob value) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void setClob(int parameterIndex, Reader reader, long length) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void setBlob(int parameterIndex, InputStream inputStream, long length) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void setNClob(int parameterIndex, Reader reader, long length) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void setSQLXML(int parameterIndex, SQLXML xmlObject) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void setObject(int parameterIndex, Object x, int targetSqlType, int scaleOrLength) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void setAsciiStream(int parameterIndex, InputStream x, long length) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void setBinaryStream(int parameterIndex, InputStream x, long length) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void setCharacterStream(int parameterIndex, Reader reader, long length) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void setAsciiStream(int parameterIndex, InputStream x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void setBinaryStream(int parameterIndex, InputStream x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void setCharacterStream(int parameterIndex, Reader reader) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void setNCharacterStream(int parameterIndex, Reader value) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void setClob(int parameterIndex, Reader reader) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void setBlob(int parameterIndex, InputStream inputStream) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void setNClob(int parameterIndex, Reader reader) throws SQLException + { + } + +} diff --git a/src/test/java/com/zaxxer/hikari/mocks/StubResultSet.java b/src/test/java/com/zaxxer/hikari/mocks/StubResultSet.java new file mode 100644 index 0000000..911daba --- /dev/null +++ b/src/test/java/com/zaxxer/hikari/mocks/StubResultSet.java @@ -0,0 +1,1292 @@ +/* + * 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.mocks; + +import java.io.InputStream; +import java.io.Reader; +import java.math.BigDecimal; +import java.net.URL; +import java.sql.Array; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.Date; +import java.sql.NClob; +import java.sql.Ref; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.RowId; +import java.sql.SQLException; +import java.sql.SQLWarning; +import java.sql.SQLXML; +import java.sql.Statement; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.Calendar; +import java.util.Map; + +/** + * + * @author Brett Wooldridge + */ +public class StubResultSet implements ResultSet +{ + private int counter; + private boolean closed; + + /** {@inheritDoc} */ + @Override + public <T> T unwrap(Class<T> iface) throws SQLException + { + + return null; + } + + /** {@inheritDoc} */ + @Override + public boolean isWrapperFor(Class<?> iface) throws SQLException + { + return false; + } + + /** {@inheritDoc} */ + @Override + public boolean next() throws SQLException + { + return (counter > 100000); + } + + /** {@inheritDoc} */ + @Override + public void close() throws SQLException + { + closed = true; + } + + /** {@inheritDoc} */ + @Override + public boolean wasNull() throws SQLException + { + return false; + } + + /** {@inheritDoc} */ + @Override + public String getString(int columnIndex) throws SQLException + { + return "aString"; + } + + /** {@inheritDoc} */ + @Override + public boolean getBoolean(int columnIndex) throws SQLException + { + return false; + } + + /** {@inheritDoc} */ + @Override + public byte getByte(int columnIndex) throws SQLException + { + return 0; + } + + /** {@inheritDoc} */ + @Override + public short getShort(int columnIndex) throws SQLException + { + return 0; + } + + /** {@inheritDoc} */ + @Override + public int getInt(int columnIndex) throws SQLException + { + return ++counter; + } + + /** {@inheritDoc} */ + @Override + public long getLong(int columnIndex) throws SQLException + { + return 0; + } + + /** {@inheritDoc} */ + @Override + public float getFloat(int columnIndex) throws SQLException + { + throw new SQLException("Simulated disconnection error", "08999"); + } + + /** {@inheritDoc} */ + @Override + public double getDouble(int columnIndex) throws SQLException + { + return 0; + } + + /** {@inheritDoc} */ + @Override + public BigDecimal getBigDecimal(int columnIndex, int scale) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public byte[] getBytes(int columnIndex) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public Date getDate(int columnIndex) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public Time getTime(int columnIndex) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public Timestamp getTimestamp(int columnIndex) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public InputStream getAsciiStream(int columnIndex) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public InputStream getUnicodeStream(int columnIndex) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public InputStream getBinaryStream(int columnIndex) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public String getString(String columnLabel) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public boolean getBoolean(String columnLabel) throws SQLException + { + return false; + } + + /** {@inheritDoc} */ + @Override + public byte getByte(String columnLabel) throws SQLException + { + return 0; + } + + /** {@inheritDoc} */ + @Override + public short getShort(String columnLabel) throws SQLException + { + return 0; + } + + /** {@inheritDoc} */ + @Override + public int getInt(String columnLabel) throws SQLException + { + return 0; + } + + /** {@inheritDoc} */ + @Override + public long getLong(String columnLabel) throws SQLException + { + return 0; + } + + /** {@inheritDoc} */ + @Override + public float getFloat(String columnLabel) throws SQLException + { + return 0; + } + + /** {@inheritDoc} */ + @Override + public double getDouble(String columnLabel) throws SQLException + { + return 0; + } + + /** {@inheritDoc} */ + @Override + public BigDecimal getBigDecimal(String columnLabel, int scale) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public byte[] getBytes(String columnLabel) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public Date getDate(String columnLabel) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public Time getTime(String columnLabel) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public Timestamp getTimestamp(String columnLabel) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public InputStream getAsciiStream(String columnLabel) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public InputStream getUnicodeStream(String columnLabel) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public InputStream getBinaryStream(String columnLabel) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public SQLWarning getWarnings() throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public void clearWarnings() throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public String getCursorName() throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public ResultSetMetaData getMetaData() throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public Object getObject(int columnIndex) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public Object getObject(String columnLabel) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public int findColumn(String columnLabel) throws SQLException + { + return 0; + } + + /** {@inheritDoc} */ + @Override + public Reader getCharacterStream(int columnIndex) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public Reader getCharacterStream(String columnLabel) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public BigDecimal getBigDecimal(int columnIndex) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public BigDecimal getBigDecimal(String columnLabel) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public boolean isBeforeFirst() throws SQLException + { + return false; + } + + /** {@inheritDoc} */ + @Override + public boolean isAfterLast() throws SQLException + { + return false; + } + + /** {@inheritDoc} */ + @Override + public boolean isFirst() throws SQLException + { + return false; + } + + /** {@inheritDoc} */ + @Override + public boolean isLast() throws SQLException + { + return false; + } + + /** {@inheritDoc} */ + @Override + public void beforeFirst() throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void afterLast() throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public boolean first() throws SQLException + { + return false; + } + + /** {@inheritDoc} */ + @Override + public boolean last() throws SQLException + { + return false; + } + + /** {@inheritDoc} */ + @Override + public int getRow() throws SQLException + { + return 0; + } + + /** {@inheritDoc} */ + @Override + public boolean absolute(int row) throws SQLException + { + return false; + } + + /** {@inheritDoc} */ + @Override + public boolean relative(int rows) throws SQLException + { + return false; + } + + /** {@inheritDoc} */ + @Override + public boolean previous() throws SQLException + { + return false; + } + + /** {@inheritDoc} */ + @Override + public void setFetchDirection(int direction) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public int getFetchDirection() throws SQLException + { + return 0; + } + + /** {@inheritDoc} */ + @Override + public void setFetchSize(int rows) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public int getFetchSize() throws SQLException + { + return 0; + } + + /** {@inheritDoc} */ + @Override + public int getType() throws SQLException + { + return 0; + } + + /** {@inheritDoc} */ + @Override + public int getConcurrency() throws SQLException + { + return 0; + } + + /** {@inheritDoc} */ + @Override + public boolean rowUpdated() throws SQLException + { + return false; + } + + /** {@inheritDoc} */ + @Override + public boolean rowInserted() throws SQLException + { + return false; + } + + /** {@inheritDoc} */ + @Override + public boolean rowDeleted() throws SQLException + { + return false; + } + + /** {@inheritDoc} */ + @Override + public void updateNull(int columnIndex) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateBoolean(int columnIndex, boolean x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateByte(int columnIndex, byte x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateShort(int columnIndex, short x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateInt(int columnIndex, int x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateLong(int columnIndex, long x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateFloat(int columnIndex, float x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateDouble(int columnIndex, double x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateBigDecimal(int columnIndex, BigDecimal x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateString(int columnIndex, String x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateBytes(int columnIndex, byte[] x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateDate(int columnIndex, Date x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateTime(int columnIndex, Time x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateTimestamp(int columnIndex, Timestamp x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateAsciiStream(int columnIndex, InputStream x, int length) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateBinaryStream(int columnIndex, InputStream x, int length) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateCharacterStream(int columnIndex, Reader x, int length) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateObject(int columnIndex, Object x, int scaleOrLength) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateObject(int columnIndex, Object x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateNull(String columnLabel) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateBoolean(String columnLabel, boolean x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateByte(String columnLabel, byte x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateShort(String columnLabel, short x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateInt(String columnLabel, int x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateLong(String columnLabel, long x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateFloat(String columnLabel, float x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateDouble(String columnLabel, double x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateBigDecimal(String columnLabel, BigDecimal x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateString(String columnLabel, String x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateBytes(String columnLabel, byte[] x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateDate(String columnLabel, Date x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateTime(String columnLabel, Time x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateTimestamp(String columnLabel, Timestamp x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateAsciiStream(String columnLabel, InputStream x, int length) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateBinaryStream(String columnLabel, InputStream x, int length) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateCharacterStream(String columnLabel, Reader reader, int length) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateObject(String columnLabel, Object x, int scaleOrLength) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateObject(String columnLabel, Object x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void insertRow() throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateRow() throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void deleteRow() throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void refreshRow() throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void cancelRowUpdates() throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void moveToInsertRow() throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void moveToCurrentRow() throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public Statement getStatement() throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public Object getObject(int columnIndex, Map<String, Class<?>> map) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public Ref getRef(int columnIndex) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public Blob getBlob(int columnIndex) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public Clob getClob(int columnIndex) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public Array getArray(int columnIndex) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public Object getObject(String columnLabel, Map<String, Class<?>> map) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public Ref getRef(String columnLabel) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public Blob getBlob(String columnLabel) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public Clob getClob(String columnLabel) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public Array getArray(String columnLabel) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public Date getDate(int columnIndex, Calendar cal) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public Date getDate(String columnLabel, Calendar cal) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public Time getTime(int columnIndex, Calendar cal) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public Time getTime(String columnLabel, Calendar cal) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public Timestamp getTimestamp(int columnIndex, Calendar cal) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public Timestamp getTimestamp(String columnLabel, Calendar cal) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public URL getURL(int columnIndex) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public URL getURL(String columnLabel) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public void updateRef(int columnIndex, Ref x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateRef(String columnLabel, Ref x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateBlob(int columnIndex, Blob x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateBlob(String columnLabel, Blob x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateClob(int columnIndex, Clob x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateClob(String columnLabel, Clob x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateArray(int columnIndex, Array x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateArray(String columnLabel, Array x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public RowId getRowId(int columnIndex) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public RowId getRowId(String columnLabel) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public void updateRowId(int columnIndex, RowId x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateRowId(String columnLabel, RowId x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public int getHoldability() throws SQLException + { + return 0; + } + + /** {@inheritDoc} */ + @Override + public boolean isClosed() throws SQLException + { + return closed; + } + + /** {@inheritDoc} */ + @Override + public void updateNString(int columnIndex, String nString) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateNString(String columnLabel, String nString) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateNClob(int columnIndex, NClob nClob) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateNClob(String columnLabel, NClob nClob) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public NClob getNClob(int columnIndex) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public NClob getNClob(String columnLabel) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public SQLXML getSQLXML(int columnIndex) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public SQLXML getSQLXML(String columnLabel) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public void updateSQLXML(int columnIndex, SQLXML xmlObject) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateSQLXML(String columnLabel, SQLXML xmlObject) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public String getNString(int columnIndex) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public String getNString(String columnLabel) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public Reader getNCharacterStream(int columnIndex) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public Reader getNCharacterStream(String columnLabel) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + @Override + public void updateNCharacterStream(int columnIndex, Reader x, long length) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateNCharacterStream(String columnLabel, Reader reader, long length) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateAsciiStream(int columnIndex, InputStream x, long length) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateBinaryStream(int columnIndex, InputStream x, long length) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateCharacterStream(int columnIndex, Reader x, long length) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateAsciiStream(String columnLabel, InputStream x, long length) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateBinaryStream(String columnLabel, InputStream x, long length) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateCharacterStream(String columnLabel, Reader reader, long length) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateBlob(int columnIndex, InputStream inputStream, long length) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateBlob(String columnLabel, InputStream inputStream, long length) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateClob(int columnIndex, Reader reader, long length) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateClob(String columnLabel, Reader reader, long length) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateNClob(int columnIndex, Reader reader, long length) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateNClob(String columnLabel, Reader reader, long length) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateNCharacterStream(int columnIndex, Reader x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateNCharacterStream(String columnLabel, Reader reader) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateAsciiStream(int columnIndex, InputStream x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateBinaryStream(int columnIndex, InputStream x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateCharacterStream(int columnIndex, Reader x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateAsciiStream(String columnLabel, InputStream x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateBinaryStream(String columnLabel, InputStream x) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateCharacterStream(String columnLabel, Reader reader) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateBlob(int columnIndex, InputStream inputStream) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateBlob(String columnLabel, InputStream inputStream) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateClob(int columnIndex, Reader reader) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateClob(String columnLabel, Reader reader) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateNClob(int columnIndex, Reader reader) throws SQLException + { + } + + /** {@inheritDoc} */ + @Override + public void updateNClob(String columnLabel, Reader reader) throws SQLException + { + } + + /** {@inheritDoc} */ + public <T> T getObject(int columnIndex, Class<T> type) throws SQLException + { + return null; + } + + /** {@inheritDoc} */ + public <T> T getObject(String columnLabel, Class<T> type) throws SQLException + { + return null; + } + +} diff --git a/src/test/java/com/zaxxer/hikari/mocks/StubStatement.java b/src/test/java/com/zaxxer/hikari/mocks/StubStatement.java new file mode 100644 index 0000000..73fec69 --- /dev/null +++ b/src/test/java/com/zaxxer/hikari/mocks/StubStatement.java @@ -0,0 +1,387 @@ +/* + * 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.mocks; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.sql.SQLWarning; +import java.sql.Statement; + +/** + * + * @author Brett Wooldridge + */ +public class StubStatement implements Statement +{ + public static volatile boolean oldDriver; + private boolean closed; + private Connection connection; + + public StubStatement(Connection connection) { + this.connection = connection; + } + + /** {@inheritDoc} */ + @SuppressWarnings("unchecked") + @Override + public <T> T unwrap(Class<T> iface) throws SQLException + { + checkClosed(); + return (T) this; + } + + /** {@inheritDoc} */ + @Override + public boolean isWrapperFor(Class<?> iface) throws SQLException + { + checkClosed(); + return false; + } + + /** {@inheritDoc} */ + @Override + public ResultSet executeQuery(String sql) throws SQLException + { + checkClosed(); + StubResultSet resultSet = new StubResultSet(); + return resultSet; + } + + /** {@inheritDoc} */ + @Override + public int executeUpdate(String sql) throws SQLException + { + checkClosed(); + return 0; + } + + /** {@inheritDoc} */ + @Override + public void close() throws SQLException + { + closed = true; + } + + /** {@inheritDoc} */ + @Override + public int getMaxFieldSize() throws SQLException + { + checkClosed(); + return 0; + } + + /** {@inheritDoc} */ + @Override + public void setMaxFieldSize(int max) throws SQLException + { + checkClosed(); + } + + /** {@inheritDoc} */ + @Override + public int getMaxRows() throws SQLException + { + checkClosed(); + return 0; + } + + /** {@inheritDoc} */ + @Override + public void setMaxRows(int max) throws SQLException + { + checkClosed(); + } + + /** {@inheritDoc} */ + @Override + public void setEscapeProcessing(boolean enable) throws SQLException + { + checkClosed(); + } + + /** {@inheritDoc} */ + @Override + public int getQueryTimeout() throws SQLException + { + checkClosed(); + return 0; + } + + /** {@inheritDoc} */ + @Override + public void setQueryTimeout(int seconds) throws SQLException + { + if (oldDriver) { + throw new SQLFeatureNotSupportedException(); + } + + checkClosed(); + } + + /** {@inheritDoc} */ + @Override + public void cancel() throws SQLException + { + checkClosed(); + } + + /** {@inheritDoc} */ + @Override + public SQLWarning getWarnings() throws SQLException + { + checkClosed(); + return null; + } + + /** {@inheritDoc} */ + @Override + public void clearWarnings() throws SQLException + { + checkClosed(); + } + + /** {@inheritDoc} */ + @Override + public void setCursorName(String name) throws SQLException + { + checkClosed(); + } + + /** {@inheritDoc} */ + @Override + public boolean execute(String sql) throws SQLException + { + checkClosed(); + return false; + } + + /** {@inheritDoc} */ + @Override + public ResultSet getResultSet() throws SQLException + { + checkClosed(); + return new StubResultSet(); + } + + /** {@inheritDoc} */ + @Override + public int getUpdateCount() throws SQLException + { + checkClosed(); + return 0; + } + + /** {@inheritDoc} */ + @Override + public boolean getMoreResults() throws SQLException + { + checkClosed(); + return false; + } + + /** {@inheritDoc} */ + @Override + public void setFetchDirection(int direction) throws SQLException + { + checkClosed(); + } + + /** {@inheritDoc} */ + @Override + public int getFetchDirection() throws SQLException + { + checkClosed(); + return 0; + } + + /** {@inheritDoc} */ + @Override + public void setFetchSize(int rows) throws SQLException + { + checkClosed(); + } + + /** {@inheritDoc} */ + @Override + public int getFetchSize() throws SQLException + { + checkClosed(); + return 0; + } + + /** {@inheritDoc} */ + @Override + public int getResultSetConcurrency() throws SQLException + { + checkClosed(); + return 0; + } + + /** {@inheritDoc} */ + @Override + public int getResultSetType() throws SQLException + { + checkClosed(); + return 0; + } + + /** {@inheritDoc} */ + @Override + public void addBatch(String sql) throws SQLException + { + checkClosed(); + } + + /** {@inheritDoc} */ + @Override + public void clearBatch() throws SQLException + { + checkClosed(); + } + + /** {@inheritDoc} */ + @Override + public int[] executeBatch() throws SQLException + { + checkClosed(); + return null; + } + + /** {@inheritDoc} */ + @Override + public Connection getConnection() throws SQLException + { + checkClosed(); + return connection; + } + + /** {@inheritDoc} */ + @Override + public boolean getMoreResults(int current) throws SQLException + { + checkClosed(); + return false; + } + + /** {@inheritDoc} */ + @Override + public ResultSet getGeneratedKeys() throws SQLException + { + checkClosed(); + return new StubResultSet(); + } + + /** {@inheritDoc} */ + @Override + public int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException + { + checkClosed(); + return 0; + } + + /** {@inheritDoc} */ + @Override + public int executeUpdate(String sql, int[] columnIndexes) throws SQLException + { + checkClosed(); + return 0; + } + + /** {@inheritDoc} */ + @Override + public int executeUpdate(String sql, String[] columnNames) throws SQLException + { + checkClosed(); + return 0; + } + + /** {@inheritDoc} */ + @Override + public boolean execute(String sql, int autoGeneratedKeys) throws SQLException + { + checkClosed(); + return false; + } + + /** {@inheritDoc} */ + @Override + public boolean execute(String sql, int[] columnIndexes) throws SQLException + { + checkClosed(); + return false; + } + + /** {@inheritDoc} */ + @Override + public boolean execute(String sql, String[] columnNames) throws SQLException + { + checkClosed(); + return false; + } + + /** {@inheritDoc} */ + @Override + public int getResultSetHoldability() throws SQLException + { + checkClosed(); + return 0; + } + + /** {@inheritDoc} */ + @Override + public boolean isClosed() throws SQLException + { + return closed; + } + + /** {@inheritDoc} */ + @Override + public void setPoolable(boolean poolable) throws SQLException + { + checkClosed(); + } + + /** {@inheritDoc} */ + @Override + public boolean isPoolable() throws SQLException + { + checkClosed(); + return false; + } + + /** {@inheritDoc} */ + public void closeOnCompletion() throws SQLException + { + checkClosed(); + } + + /** {@inheritDoc} */ + public boolean isCloseOnCompletion() throws SQLException + { + checkClosed(); + return false; + } + + private void checkClosed() throws SQLException + { + if (closed) { + throw new SQLException("Statement is closed"); + } + } +} diff --git a/src/test/java/com/zaxxer/hikari/osgi/OSGiBundleTest.java b/src/test/java/com/zaxxer/hikari/osgi/OSGiBundleTest.java new file mode 100644 index 0000000..761a802 --- /dev/null +++ b/src/test/java/com/zaxxer/hikari/osgi/OSGiBundleTest.java @@ -0,0 +1,91 @@ +/* + * 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.osgi; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.Configuration; +import org.ops4j.pax.exam.Option; +import org.ops4j.pax.exam.junit.PaxExam; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; + +import javax.inject.Inject; + +import java.io.File; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.ops4j.pax.exam.CoreOptions.*; + +/** + * @author lburgazzoli + */ +@RunWith(PaxExam.class) +public class OSGiBundleTest +{ + @Inject + BundleContext context; + + @Configuration + public Option[] config() + { + return options( + systemProperty("org.osgi.framework.storage.clean").value("true"), + systemProperty("org.ops4j.pax.logging.DefaultServiceLog.level").value("WARN"), + mavenBundle("org.slf4j","slf4j-api","1.7.5"), + mavenBundle("org.slf4j","slf4j-simple","1.7.5").noStart(), + mavenBundle("org.javassist", "javassist", "3.19.0-GA"), + new File("target/classes").exists() + ? bundle("reference:file:target/classes") + : bundle("reference:file:../target/classes"), + junitBundles(), + cleanCaches() + ); + } + + @Test + public void checkInject() + { + assertNotNull(context); + } + + @Test + public void checkBundle() + { + Boolean bundleFound = false; + Boolean bundleActive = false; + + Bundle[] bundles = context.getBundles(); + for(Bundle bundle : bundles) + { + if(bundle != null) + { + if(bundle.getSymbolicName().equals("com.zaxxer.HikariCP")) + { + bundleFound = true; + if(bundle.getState() == Bundle.ACTIVE) + { + bundleActive = true; + } + } + } + } + + assertTrue(bundleFound); + assertTrue(bundleActive); + } +} diff --git a/src/test/java/com/zaxxer/hikari/pool/ConcurrentCloseConnectionTest.java b/src/test/java/com/zaxxer/hikari/pool/ConcurrentCloseConnectionTest.java new file mode 100644 index 0000000..8820b79 --- /dev/null +++ b/src/test/java/com/zaxxer/hikari/pool/ConcurrentCloseConnectionTest.java @@ -0,0 +1,76 @@ +/* + * 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.pool.TestElf.newHikariConfig; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +import org.junit.Test; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; + +/** + * @author Matthew Tambara (matthew.tambara@liferay.com) + */ +public class ConcurrentCloseConnectionTest +{ + @Test + public void testConcurrentClose() throws Exception + { + HikariConfig config = newHikariConfig(); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + try (HikariDataSource ds = new HikariDataSource(config); + final Connection connection = ds.getConnection()) { + + ExecutorService executorService = Executors.newFixedThreadPool(10); + + List<Future<?>> futures = new ArrayList<>(); + + for (int i = 0; i < 500; i++) { + final PreparedStatement preparedStatement = + connection.prepareStatement(""); + + futures.add(executorService.submit(new Callable<Void>() { + + @Override + public Void call() throws Exception { + preparedStatement.close(); + + return null; + } + + })); + } + + executorService.shutdown(); + + for (Future<?> future : futures) { + future.get(); + } + } + } +} diff --git a/src/test/java/com/zaxxer/hikari/pool/ConnectionPoolSizeVsThreadsTest.java b/src/test/java/com/zaxxer/hikari/pool/ConnectionPoolSizeVsThreadsTest.java new file mode 100755 index 0000000..e66ba58 --- /dev/null +++ b/src/test/java/com/zaxxer/hikari/pool/ConnectionPoolSizeVsThreadsTest.java @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2013, 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.pool; + +import static com.zaxxer.hikari.pool.TestElf.getPool; +import static com.zaxxer.hikari.pool.TestElf.newHikariConfig; +import static com.zaxxer.hikari.util.ClockSource.currentTime; +import static com.zaxxer.hikari.util.ClockSource.elapsedMillis; +import static com.zaxxer.hikari.util.UtilityElf.quietlySleep; +import static java.util.concurrent.Executors.newFixedThreadPool; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.sql.Connection; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import com.zaxxer.hikari.mocks.StubDataSource; + +/** + * @author Matthew Tambara (matthew.tambara@liferay.com) + */ +public class ConnectionPoolSizeVsThreadsTest { + + public static final Logger LOGGER = LoggerFactory.getLogger(ConnectionPoolSizeVsThreadsTest.class); + + public static final int ITERATIONS = 50_000; + + @Test + public void testPoolSizeAboutSameSizeAsThreadCount() throws Exception { + final int threadCount = 50; + final Counts counts = testPoolSize(2 /*minIdle*/, + 100 /*maxPoolSize*/, + threadCount, + 1 /*workTimeMs*/, + 0 /*restTimeMs*/, + 20 /*connectionAcquisitionTimeMs*/, + ITERATIONS, + SECONDS.toMillis(2) /*postTestTimeMs*/); + + // maxActive may never make it to threadCount but it shouldn't be any higher + assertEquals(threadCount, counts.maxActive, 15 /*delta*/); + assertEquals(threadCount, counts.maxTotal, 5 /*delta*/); + } + + @Test + public void testSlowConnectionTimeBurstyWork() throws Exception { + // setup a bursty work load, 50 threads all needing to do around 100 units of work. + // Using a more realistic time for connection startup of 250 ms and only 5 seconds worth of work will mean that we end up finishing + // all of the work before we actually have setup 50 connections even though we have requested 50 connections + final int threadCount = 50; + final int workItems = threadCount * 100; + final int workTimeMs = 0; + final int connectionAcquisitionTimeMs = 250; + final Counts counts = testPoolSize(2 /*minIdle*/, + 100 /*maxPoolSize*/, + threadCount, + workTimeMs, + 0 /*restTimeMs*/, + connectionAcquisitionTimeMs, + workItems /*iterations*/, + SECONDS.toMillis(3) /*postTestTimeMs*/); + + // hard to put exact bounds on how many thread we will use but we can put an upper bound on usage (if there was only one thread) + final long totalWorkTime = workItems * workTimeMs; + final long connectionMax = totalWorkTime / connectionAcquisitionTimeMs; + assertTrue(connectionMax <= counts.maxActive); + assertEquals(connectionMax, counts.maxTotal, 2 + 2 /*delta*/); + } + + private Counts testPoolSize(final int minIdle, final int maxPoolSize, final int threadCount, + final long workTimeMs, final long restTimeMs, final long connectionAcquisitionTimeMs, + final int iterations, final long postTestTimeMs) throws Exception { + + LOGGER.info("Starting test (minIdle={}, maxPoolSize={}, threadCount={}, workTimeMs={}, restTimeMs={}, connectionAcquisitionTimeMs={}, iterations={}, postTestTimeMs={})", + minIdle, maxPoolSize, threadCount, workTimeMs, restTimeMs, connectionAcquisitionTimeMs, iterations, postTestTimeMs); + + final HikariConfig config = newHikariConfig(); + config.setMinimumIdle(minIdle); + config.setMaximumPoolSize(maxPoolSize); + config.setInitializationFailTimeout(Long.MAX_VALUE); + config.setConnectionTimeout(2500); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + final AtomicReference<Exception> ref = new AtomicReference<>(null); + + // Initialize HikariPool with no initial connections and room to grow + try (final HikariDataSource ds = new HikariDataSource(config)) { + final StubDataSource stubDataSource = ds.unwrap(StubDataSource.class); + // connection acquisition takes more than 0 ms in a real system + stubDataSource.setConnectionAcquistionTime(connectionAcquisitionTimeMs); + + final ExecutorService threadPool = newFixedThreadPool(threadCount); + final CountDownLatch allThreadsDone = new CountDownLatch(iterations); + for (int i = 0; i < iterations; i++) { + threadPool.submit(() -> { + if (ref.get() == null) { + quietlySleep(restTimeMs); + try (Connection c2 = ds.getConnection()) { + quietlySleep(workTimeMs); + } + catch (Exception e) { + ref.set(e); + } + } + allThreadsDone.countDown(); + }); + } + + final HikariPool pool = getPool(ds); + + // collect pool usage data while work is still being done + final Counts underLoad = new Counts(); + while (allThreadsDone.getCount() > 0 || pool.getTotalConnections() < minIdle) { + quietlySleep(50); + underLoad.updateMaxCounts(pool); + } + + // wait for long enough any pending acquisitions have already been done + LOGGER.info("Test Over, waiting for post delay time {}ms ", postTestTimeMs); + quietlySleep(connectionAcquisitionTimeMs + workTimeMs + restTimeMs); + + // collect pool data while there is no work to do. + final Counts postLoad = new Counts(); + final long start = currentTime(); + while (elapsedMillis(start) < postTestTimeMs) { + quietlySleep(50); + postLoad.updateMaxCounts(pool); + } + + allThreadsDone.await(); + + threadPool.shutdown(); + threadPool.awaitTermination(30, SECONDS); + + if (ref.get() != null) { + LOGGER.error("Task failed", ref.get()); + fail("Task failed"); + } + + LOGGER.info("Under Load... {}", underLoad); + LOGGER.info("Post Load.... {}", postLoad); + + // verify that the no connections created after the work has stopped + if (postTestTimeMs > 0) { + if (postLoad.maxActive != 0) { + fail("Max Active was greater than 0 after test was done"); + } + + final int createdAfterWorkAllFinished = postLoad.maxTotal - underLoad.maxTotal; + assertEquals("Connections were created when there was no waiting consumers", 0, createdAfterWorkAllFinished, 1 /*delta*/); + } + + return underLoad; + } + } + + private static class Counts { + int maxTotal = 0; + int maxActive = 0; + + void updateMaxCounts(final HikariPool pool) { + maxTotal = Math.max(pool.getTotalConnections(), maxTotal); + maxActive = Math.max(pool.getActiveConnections(), maxActive); + } + + @Override + public String toString() { + return "Counts{" + + "maxTotal=" + maxTotal + + ", maxActive=" + maxActive + + '}'; + } + } +} diff --git a/src/test/java/com/zaxxer/hikari/pool/ConnectionRaceConditionTest.java b/src/test/java/com/zaxxer/hikari/pool/ConnectionRaceConditionTest.java new file mode 100755 index 0000000..31f8363 --- /dev/null +++ b/src/test/java/com/zaxxer/hikari/pool/ConnectionRaceConditionTest.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 static com.zaxxer.hikari.pool.TestElf.newHikariConfig; +import static com.zaxxer.hikari.pool.TestElf.setSlf4jLogLevel; +import static org.junit.Assert.fail; + +import java.sql.Connection; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.logging.log4j.Level; +import org.junit.After; +import org.junit.Test; +import org.slf4j.LoggerFactory; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import com.zaxxer.hikari.util.ConcurrentBag; + +/** + * @author Matthew Tambara (matthew.tambara@liferay.com) + */ +public class ConnectionRaceConditionTest +{ + + public static final int ITERATIONS = 10_000; + + @Test + public void testRaceCondition() throws Exception + { + HikariConfig config = newHikariConfig(); + config.setMinimumIdle(0); + config.setMaximumPoolSize(10); + config.setInitializationFailTimeout(Long.MAX_VALUE); + config.setConnectionTimeout(2500); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + setSlf4jLogLevel(ConcurrentBag.class, Level.INFO); + + final AtomicReference<Exception> ref = new AtomicReference<>(null); + + // Initialize HikariPool with no initial connections and room to grow + try (final HikariDataSource ds = new HikariDataSource(config)) { + ExecutorService threadPool = Executors.newFixedThreadPool(2); + for (int i = 0; i < ITERATIONS; i++) { + threadPool.submit(new Callable<Exception>() { + /** {@inheritDoc} */ + @Override + public Exception call() throws Exception + { + if (ref.get() == null) { + Connection c2; + try { + c2 = ds.getConnection(); + ds.evictConnection(c2); + } + catch (Exception e) { + ref.set(e); + } + } + return null; + } + }); + } + + threadPool.shutdown(); + threadPool.awaitTermination(30, TimeUnit.SECONDS); + + if (ref.get() != null) { + LoggerFactory.getLogger(ConnectionRaceConditionTest.class).error("Task failed", ref.get()); + fail("Task failed"); + } + } + catch (Exception e) { + throw e; + } + } + + @After + public void after() + { + System.getProperties().remove("com.zaxxer.hikari.housekeeping.periodMs"); + + setSlf4jLogLevel(HikariPool.class, Level.WARN); + setSlf4jLogLevel(ConcurrentBag.class, Level.WARN); + } +} diff --git a/src/test/java/com/zaxxer/hikari/pool/ConnectionStateTest.java b/src/test/java/com/zaxxer/hikari/pool/ConnectionStateTest.java new file mode 100644 index 0000000..35b36fb --- /dev/null +++ b/src/test/java/com/zaxxer/hikari/pool/ConnectionStateTest.java @@ -0,0 +1,170 @@ +/* + * 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.pool.TestElf.newHikariConfig; +import static com.zaxxer.hikari.pool.TestElf.newHikariDataSource; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import org.junit.Test; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import com.zaxxer.hikari.util.UtilityElf; + +public class ConnectionStateTest +{ + @Test + public void testAutoCommit() throws SQLException + { + try (HikariDataSource ds = newHikariDataSource()) { + ds.setAutoCommit(true); + ds.setMinimumIdle(1); + ds.setMaximumPoolSize(1); + ds.setConnectionTestQuery("VALUES 1"); + ds.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + ds.addDataSourceProperty("user", "bar"); + ds.addDataSourceProperty("password", "secret"); + ds.addDataSourceProperty("url", "baf"); + ds.addDataSourceProperty("loginTimeout", "10"); + + try (Connection connection = ds.getConnection()) { + Connection unwrap = connection.unwrap(Connection.class); + connection.setAutoCommit(false); + connection.close(); + + assertTrue(unwrap.getAutoCommit()); + } + } + } + + @Test + public void testTransactionIsolation() throws SQLException + { + try (HikariDataSource ds = newHikariDataSource()) { + ds.setTransactionIsolation("TRANSACTION_READ_COMMITTED"); + ds.setMinimumIdle(1); + ds.setMaximumPoolSize(1); + ds.setConnectionTestQuery("VALUES 1"); + ds.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + try (Connection connection = ds.getConnection()) { + Connection unwrap = connection.unwrap(Connection.class); + connection.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED); + connection.close(); + + assertEquals(Connection.TRANSACTION_READ_COMMITTED, unwrap.getTransactionIsolation()); + } + } + } + + @Test + public void testIsolation() throws Exception + { + HikariConfig config = newHikariConfig(); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + config.setTransactionIsolation("TRANSACTION_REPEATABLE_READ"); + config.validate(); + + int transactionIsolation = UtilityElf.getTransactionIsolation(config.getTransactionIsolation()); + assertSame(Connection.TRANSACTION_REPEATABLE_READ, transactionIsolation); + } + + @Test + public void testReadOnly() throws Exception + { + try (HikariDataSource ds = newHikariDataSource()) { + ds.setCatalog("test"); + ds.setMinimumIdle(1); + ds.setMaximumPoolSize(1); + ds.setConnectionTestQuery("VALUES 1"); + ds.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + try (Connection connection = ds.getConnection()) { + Connection unwrap = connection.unwrap(Connection.class); + connection.setReadOnly(true); + connection.close(); + + assertFalse(unwrap.isReadOnly()); + } + } + } + + @Test + public void testCatalog() throws SQLException + { + try (HikariDataSource ds = newHikariDataSource()) { + ds.setCatalog("test"); + ds.setMinimumIdle(1); + ds.setMaximumPoolSize(1); + ds.setConnectionTestQuery("VALUES 1"); + ds.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + try (Connection connection = ds.getConnection()) { + Connection unwrap = connection.unwrap(Connection.class); + connection.setCatalog("other"); + connection.close(); + + assertEquals("test", unwrap.getCatalog()); + } + } + } + + @Test + public void testCommitTracking() throws SQLException + { + try (HikariDataSource ds = newHikariDataSource()) { + ds.setAutoCommit(false); + ds.setMinimumIdle(1); + ds.setMaximumPoolSize(1); + ds.setConnectionTestQuery("VALUES 1"); + ds.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + try (Connection connection = ds.getConnection()) { + Statement statement = connection.createStatement(); + statement.execute("SELECT something"); + assertTrue(TestElf.getConnectionCommitDirtyState(connection)); + + connection.commit(); + assertFalse(TestElf.getConnectionCommitDirtyState(connection)); + + statement.execute("SELECT something", Statement.NO_GENERATED_KEYS); + assertTrue(TestElf.getConnectionCommitDirtyState(connection)); + + connection.rollback(); + assertFalse(TestElf.getConnectionCommitDirtyState(connection)); + + ResultSet resultSet = statement.executeQuery("SELECT something"); + assertTrue(TestElf.getConnectionCommitDirtyState(connection)); + + connection.rollback(null); + assertFalse(TestElf.getConnectionCommitDirtyState(connection)); + + resultSet.updateRow(); + assertTrue(TestElf.getConnectionCommitDirtyState(connection)); + } + } + } +} diff --git a/src/test/java/com/zaxxer/hikari/pool/ExceptionTest.java b/src/test/java/com/zaxxer/hikari/pool/ExceptionTest.java new file mode 100644 index 0000000..8bd64b0 --- /dev/null +++ b/src/test/java/com/zaxxer/hikari/pool/ExceptionTest.java @@ -0,0 +1,119 @@ +/* + * 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.pool.TestElf.newHikariConfig; +import static com.zaxxer.hikari.pool.TestElf.getPool; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; + +public class ExceptionTest +{ + private HikariDataSource ds; + + @Before + public void setup() + { + HikariConfig config = newHikariConfig(); + config.setMinimumIdle(1); + config.setMaximumPoolSize(2); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + ds = new HikariDataSource(config); + } + + @After + public void teardown() + { + ds.close(); + } + + @Test + public void testException1() throws SQLException + { + try (Connection connection = ds.getConnection()) { + assertNotNull(connection); + + PreparedStatement statement = connection.prepareStatement("SELECT some, thing FROM somewhere WHERE something=?"); + assertNotNull(statement); + + ResultSet resultSet = statement.executeQuery(); + assertNotNull(resultSet); + + try { + statement.getMaxFieldSize(); + fail(); + } + catch (Exception e) { + assertSame(SQLException.class, e.getClass()); + } + } + + HikariPool pool = getPool(ds); + assertTrue("Total (3) connections not as expected", pool.getTotalConnections() >= 0); + assertTrue("Idle (3) connections not as expected", pool.getIdleConnections() >= 0); + } + + @Test + public void testUseAfterStatementClose() throws SQLException + { + Connection connection = ds.getConnection(); + assertNotNull(connection); + + try (Statement statement = connection.prepareStatement("SELECT some, thing FROM somewhere WHERE something=?")) { + statement.close(); + statement.getMoreResults(); + + fail(); + } + catch (SQLException e) { + assertSame("Connection is closed", e.getMessage()); + } + } + + @Test + public void testUseAfterClose() throws SQLException + { + try (Connection connection = ds.getConnection()) { + assertNotNull(connection); + connection.close(); + + try (Statement statement = connection.prepareStatement("SELECT some, thing FROM somewhere WHERE something=?")) { + fail(); + } + catch (SQLException e) { + assertSame("Connection is closed", e.getMessage()); + } + } + } +} diff --git a/src/test/java/com/zaxxer/hikari/pool/HouseKeeperCleanupTest.java b/src/test/java/com/zaxxer/hikari/pool/HouseKeeperCleanupTest.java new file mode 100644 index 0000000..1a7caa7 --- /dev/null +++ b/src/test/java/com/zaxxer/hikari/pool/HouseKeeperCleanupTest.java @@ -0,0 +1,84 @@ +/* + * 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.pool.TestElf.newHikariConfig; +import static org.junit.Assert.assertEquals; + +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import com.zaxxer.hikari.util.UtilityElf; + +/** + * @author Martin Stříž (striz@raynet.cz) + */ +public class HouseKeeperCleanupTest +{ + + private ScheduledThreadPoolExecutor executor; + + @Before + public void before() throws Exception + { + ThreadFactory threadFactory = new UtilityElf.DefaultThreadFactory("global housekeeper", true); + + executor = new ScheduledThreadPoolExecutor(1, threadFactory, new ThreadPoolExecutor.DiscardPolicy()); + executor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false); + executor.setRemoveOnCancelPolicy(true); + } + + @Test + public void testHouseKeeperCleanupWithCustomExecutor() throws Exception + { + HikariConfig config = newHikariConfig(); + config.setMinimumIdle(0); + config.setMaximumPoolSize(10); + config.setInitializationFailTimeout(Long.MAX_VALUE); + config.setConnectionTimeout(2500); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + config.setScheduledExecutorService(executor); + + HikariConfig config2 = newHikariConfig(); + config.copyState(config2); + + try ( + final HikariDataSource ds1 = new HikariDataSource(config); + final HikariDataSource ds2 = new HikariDataSource(config2) + ) { + assertEquals("Scheduled tasks count not as expected, ", 2, executor.getQueue().size()); + } + + assertEquals("Scheduled tasks count not as expected, ", 0, executor.getQueue().size()); + } + + @After + public void after() throws Exception + { + executor.shutdown(); + executor.awaitTermination(5, TimeUnit.SECONDS); + } + +} diff --git a/src/test/java/com/zaxxer/hikari/pool/IsolationTest.java b/src/test/java/com/zaxxer/hikari/pool/IsolationTest.java new file mode 100644 index 0000000..01a80ae --- /dev/null +++ b/src/test/java/com/zaxxer/hikari/pool/IsolationTest.java @@ -0,0 +1,74 @@ +/* + * 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.pool.TestElf.newHikariDataSource; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; + +import java.sql.Connection; +import java.sql.SQLException; + +import org.junit.Test; + +import com.zaxxer.hikari.HikariDataSource; + +public class IsolationTest +{ + @Test + public void testIsolation() throws SQLException + { + try (HikariDataSource ds = newHikariDataSource()) { + ds.setMinimumIdle(1); + ds.setMaximumPoolSize(1); + ds.setIsolateInternalQueries(true); + ds.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + try (Connection connection = ds.getConnection()) { + connection.close(); + + try (Connection connection2 = ds.getConnection()) { + connection2.close(); + + assertNotSame(connection, connection2); + assertSame(connection.unwrap(Connection.class), connection2.unwrap(Connection.class)); + } + } + } + } + + @Test + public void testNonIsolation() throws SQLException + { + try (HikariDataSource ds = newHikariDataSource()) { + ds.setMinimumIdle(1); + ds.setMaximumPoolSize(1); + ds.setIsolateInternalQueries(false); + ds.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + try (Connection connection = ds.getConnection()) { + connection.close(); + + try (Connection connection2 = ds.getConnection()) { + connection2.close(); + + assertSame(connection.unwrap(Connection.class), connection2.unwrap(Connection.class)); + } + } + } + } +} diff --git a/src/test/java/com/zaxxer/hikari/pool/JdbcDriverTest.java b/src/test/java/com/zaxxer/hikari/pool/JdbcDriverTest.java new file mode 100644 index 0000000..0d9cc2f --- /dev/null +++ b/src/test/java/com/zaxxer/hikari/pool/JdbcDriverTest.java @@ -0,0 +1,86 @@ +/* + * 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.pool.TestElf.newHikariConfig; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.sql.Connection; +import java.sql.SQLException; + +import org.junit.After; +import org.junit.Test; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import com.zaxxer.hikari.util.DriverDataSource; + +public class JdbcDriverTest +{ + private HikariDataSource ds; + + @After + public void teardown() + { + if (ds != null) { + ds.close(); + } + } + + @Test + public void driverTest1() throws SQLException + { + HikariConfig config = newHikariConfig(); + config.setMinimumIdle(1); + config.setMaximumPoolSize(1); + config.setConnectionTestQuery("VALUES 1"); + config.setDriverClassName("com.zaxxer.hikari.mocks.StubDriver"); + config.setJdbcUrl("jdbc:stub"); + config.addDataSourceProperty("user", "bart"); + config.addDataSourceProperty("password", "simpson"); + + ds = new HikariDataSource(config); + + assertTrue(ds.isWrapperFor(DriverDataSource.class)); + + DriverDataSource unwrap = ds.unwrap(DriverDataSource.class); + assertNotNull(unwrap); + + try (Connection connection = ds.getConnection()) { + // test that getConnection() succeeds + } + } + + @Test + public void driverTest2() throws SQLException + { + HikariConfig config = newHikariConfig(); + + config.setMinimumIdle(1); + config.setMaximumPoolSize(1); + config.setConnectionTestQuery("VALUES 1"); + config.setDriverClassName("com.zaxxer.hikari.mocks.StubDriver"); + config.setJdbcUrl("jdbc:invalid"); + + try { + ds = new HikariDataSource(config); + } + catch (RuntimeException e) { + assertTrue(e.getMessage().contains("claims to not accept")); + } + } +} diff --git a/src/test/java/com/zaxxer/hikari/pool/MiscTest.java b/src/test/java/com/zaxxer/hikari/pool/MiscTest.java new file mode 100644 index 0000000..f9698ef --- /dev/null +++ b/src/test/java/com/zaxxer/hikari/pool/MiscTest.java @@ -0,0 +1,134 @@ +/* + * 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 static com.zaxxer.hikari.pool.TestElf.newHikariConfig; +import static com.zaxxer.hikari.pool.TestElf.getPool; +import static com.zaxxer.hikari.pool.TestElf.setConfigUnitTest; +import static com.zaxxer.hikari.pool.TestElf.setSlf4jLogLevel; +import static com.zaxxer.hikari.pool.TestElf.setSlf4jTargetStream; +import static com.zaxxer.hikari.util.UtilityElf.createInstance; +import static com.zaxxer.hikari.util.UtilityElf.getTransactionIsolation; +import static com.zaxxer.hikari.util.UtilityElf.quietlySleep; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import org.apache.logging.log4j.Level; +import org.junit.Test; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; + +/** + * @author Brett Wooldridge + */ +public class MiscTest +{ + @Test + public void testLogWriter() throws SQLException + { + HikariConfig config = newHikariConfig(); + config.setMinimumIdle(0); + config.setMaximumPoolSize(4); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + setConfigUnitTest(true); + + try (HikariDataSource ds = new HikariDataSource(config)) { + PrintWriter writer = new PrintWriter(System.out); + ds.setLogWriter(writer); + assertSame(writer, ds.getLogWriter()); + assertEquals("testLogWriter", config.getPoolName()); + } + finally + { + setConfigUnitTest(false); + } + } + + @Test + public void testInvalidIsolation() + { + try { + getTransactionIsolation("INVALID"); + fail(); + } + catch (Exception e) { + assertTrue(e instanceof IllegalArgumentException); + } + } + + @Test + public void testCreateInstance() + { + try { + createInstance("invalid", null); + fail(); + } + catch (RuntimeException e) { + assertTrue(e.getCause() instanceof ClassNotFoundException); + } + } + + @Test + public void testLeakDetection() throws Exception + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (PrintStream ps = new PrintStream(baos, true)) { + setSlf4jTargetStream(Class.forName("com.zaxxer.hikari.pool.ProxyLeakTask"), ps); + setConfigUnitTest(true); + + HikariConfig config = newHikariConfig(); + config.setMinimumIdle(0); + config.setMaximumPoolSize(4); + config.setThreadFactory(Executors.defaultThreadFactory()); + config.setMetricRegistry(null); + config.setLeakDetectionThreshold(TimeUnit.SECONDS.toMillis(1)); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + try (HikariDataSource ds = new HikariDataSource(config)) { + setSlf4jLogLevel(HikariPool.class, Level.DEBUG); + getPool(ds).logPoolState(); + + try (Connection connection = ds.getConnection()) { + quietlySleep(SECONDS.toMillis(4)); + connection.close(); + quietlySleep(SECONDS.toMillis(1)); + ps.close(); + String s = new String(baos.toByteArray()); + assertNotNull("Exception string was null", s); + assertTrue("Expected exception to contain 'Connection leak detection' but contains *" + s + "*", s.contains("Connection leak detection")); + } + } + finally + { + setConfigUnitTest(false); + } + } + } +} diff --git a/src/test/java/com/zaxxer/hikari/pool/PostgresTest.java b/src/test/java/com/zaxxer/hikari/pool/PostgresTest.java new file mode 100644 index 0000000..693e7b3 --- /dev/null +++ b/src/test/java/com/zaxxer/hikari/pool/PostgresTest.java @@ -0,0 +1,231 @@ +/* + * 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.pool.TestElf.getPool; +import static com.zaxxer.hikari.pool.TestElf.newHikariConfig; +import static com.zaxxer.hikari.util.ClockSource.currentTime; +import static com.zaxxer.hikari.util.ClockSource.elapsedMillis; +import static com.zaxxer.hikari.util.UtilityElf.quietlySleep; +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.junit.Before; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import com.zaxxer.hikari.util.UtilityElf; + +/** + * This test is meant to be run manually and interactively and was + * build for issue #159. + * + * @author Brett Wooldridge + */ +public class PostgresTest +{ + //@Test + public void testCase1() throws Exception + { + HikariConfig config = newHikariConfig(); + config.setMinimumIdle(3); + config.setMaximumPoolSize(10); + config.setConnectionTimeout(3000); + config.setIdleTimeout(TimeUnit.SECONDS.toMillis(10)); + config.setValidationTimeout(TimeUnit.SECONDS.toMillis(2)); + + config.setJdbcUrl("jdbc:pgsql://localhost:5432/test"); + config.setUsername("brettw"); + + try (final HikariDataSource ds = new HikariDataSource(config)) { + final long start = currentTime(); + do { + Thread t = new Thread() { + public void run() { + try (Connection connection = ds.getConnection()) { + System.err.println("Obtained connection " + connection); + quietlySleep(TimeUnit.SECONDS.toMillis((long)(10 + (Math.random() * 20)))); + } + catch (SQLException e) { + e.printStackTrace(); + } + } + }; + t.setDaemon(true); + t.start(); + + quietlySleep(TimeUnit.SECONDS.toMillis((long)((Math.random() * 20)))); + } while (elapsedMillis(start) < MINUTES.toMillis(15)); + } + } + + //@Test + public void testCase2() throws Exception + { + HikariConfig config = newHikariConfig(); + config.setMinimumIdle(3); + config.setMaximumPoolSize(10); + config.setConnectionTimeout(1000); + config.setIdleTimeout(TimeUnit.SECONDS.toMillis(60)); + + config.setJdbcUrl("jdbc:pgsql://localhost:5432/test"); + config.setUsername("brettw"); + + try (HikariDataSource ds = new HikariDataSource(config)) { + + try (Connection conn = ds.getConnection()) { + System.err.println("\nGot a connection, and released it. Now, enable the firewall."); + } + + getPool(ds).logPoolState(); + quietlySleep(5000L); + + System.err.println("\nNow attempting another getConnection(), expecting a timeout..."); + + long start = currentTime(); + try (Connection conn = ds.getConnection()) { + System.err.println("\nOpps, got a connection. Did you enable the firewall? " + conn); + fail("Opps, got a connection. Did you enable the firewall?"); + } + catch (SQLException e) + { + assertTrue("Timeout less than expected " + elapsedMillis(start) + "ms", elapsedMillis(start) > 5000); + } + + System.err.println("\nOk, so far so good. Now, disable the firewall again. Attempting connection in 5 seconds..."); + quietlySleep(5000L); + getPool(ds).logPoolState(); + + try (Connection conn = ds.getConnection()) { + System.err.println("\nGot a connection, and released it."); + } + } + + System.err.println("\nPassed."); + } + + //@Test + public void testCase3() throws Exception + { + HikariConfig config = newHikariConfig(); + config.setMinimumIdle(3); + config.setMaximumPoolSize(10); + config.setConnectionTimeout(1000); + config.setIdleTimeout(TimeUnit.SECONDS.toMillis(60)); + + config.setJdbcUrl("jdbc:pgsql://localhost:5432/test"); + config.setUsername("brettw"); + + try (final HikariDataSource ds = new HikariDataSource(config)) { + for (int i = 0; i < 10; i++) { + new Thread() { + public void run() { + try (Connection conn = ds.getConnection()) { + System.err.println("ERROR: should not have acquired connection."); + } + catch (SQLException e) { + // expected + } + } + }.start(); + } + + quietlySleep(5000L); + + System.err.println("Now, bring the DB online. Checking pool in 15 seconds."); + quietlySleep(15000L); + + getPool(ds).logPoolState(); + } + } + + // @Test + public void testCase4() throws Exception + { + HikariConfig config = newHikariConfig(); + config.setMinimumIdle(0); + config.setMaximumPoolSize(15); + config.setConnectionTimeout(10000); + config.setIdleTimeout(TimeUnit.MINUTES.toMillis(1)); + config.setMaxLifetime(TimeUnit.MINUTES.toMillis(2)); + config.setRegisterMbeans(true); + + config.setJdbcUrl("jdbc:postgresql://localhost:5432/netld"); + config.setUsername("brettw"); + + try (final HikariDataSource ds = new HikariDataSource(config)) { + + countdown(20); + List<Thread> threads = new ArrayList<>(); + for (int i = 0; i < 20; i++) { + threads.add(new Thread() { + public void run() { + UtilityElf.quietlySleep((long)(Math.random() * 2500L)); + final long start = currentTime(); + do { + try (Connection conn = ds.getConnection(); Statement stmt = conn.createStatement()) { + try (ResultSet rs = stmt.executeQuery("SELECT * FROM device WHERE device_id=0 ORDER BY device_id LIMIT 1 OFFSET 0")) { + rs.next(); + } + UtilityElf.quietlySleep(100L); //Math.max(50L, (long)(Math.random() * 250L))); + } + catch (SQLException e) { + e.printStackTrace(); + // throw new RuntimeException(e); + } + + // UtilityElf.quietlySleep(10L); //Math.max(50L, (long)(Math.random() * 250L))); + } while (elapsedMillis(start) < TimeUnit.MINUTES.toMillis(5)); + } + }); + } + +// threads.forEach(t -> t.start()); +// threads.forEach(t -> { try { t.join(); } catch (InterruptedException e) {} }); + } + } + + @Before + public void before() + { + System.err.println("\n"); + } + + private void countdown(int seconds) + { + do { + System.out.printf("Starting in %d seconds...\n", seconds); + if (seconds > 10) { + UtilityElf.quietlySleep(TimeUnit.SECONDS.toMillis(10)); + seconds -= 10; + } + else { + UtilityElf.quietlySleep(TimeUnit.SECONDS.toMillis(1)); + seconds -= 1; + } + } while (seconds > 0); + } +} diff --git a/src/test/java/com/zaxxer/hikari/pool/RampUpDown.java b/src/test/java/com/zaxxer/hikari/pool/RampUpDown.java new file mode 100755 index 0000000..e09894d --- /dev/null +++ b/src/test/java/com/zaxxer/hikari/pool/RampUpDown.java @@ -0,0 +1,75 @@ +/* + * 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.pool.TestElf.newHikariConfig; +import static com.zaxxer.hikari.pool.TestElf.getPool; +import static com.zaxxer.hikari.util.UtilityElf.quietlySleep; +import static org.junit.Assert.assertSame; + +import java.sql.Connection; +import java.sql.SQLException; + +import org.junit.Assert; +import org.junit.Test; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; + +public class RampUpDown +{ + @Test + public void rampUpDownTest() throws SQLException + { + HikariConfig config = newHikariConfig(); + config.setMinimumIdle(5); + config.setMaximumPoolSize(60); + config.setInitializationFailTimeout(0); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + System.setProperty("com.zaxxer.hikari.housekeeping.periodMs", "250"); + + try (HikariDataSource ds = new HikariDataSource(config)) { + + ds.setIdleTimeout(1000); + HikariPool pool = getPool(ds); + + // wait two housekeeping periods so we don't fail if this part of test runs too quickly + quietlySleep(500); + + Assert.assertSame("Total connections not as expected", 5, pool.getTotalConnections()); + + Connection[] connections = new Connection[ds.getMaximumPoolSize()]; + for (int i = 0; i < connections.length; i++) + { + connections[i] = ds.getConnection(); + } + + assertSame("Total connections not as expected", 60, pool.getTotalConnections()); + + for (Connection connection : connections) + { + connection.close(); + } + + quietlySleep(500); + + assertSame("Total connections not as expected", 5, pool.getTotalConnections()); + } + } +} diff --git a/src/test/java/com/zaxxer/hikari/pool/ShutdownTest.java b/src/test/java/com/zaxxer/hikari/pool/ShutdownTest.java new file mode 100644 index 0000000..aa12a43 --- /dev/null +++ b/src/test/java/com/zaxxer/hikari/pool/ShutdownTest.java @@ -0,0 +1,356 @@ +/* + * 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.pool.TestElf.getPool; +import static com.zaxxer.hikari.pool.TestElf.newHikariConfig; +import static com.zaxxer.hikari.pool.TestElf.setSlf4jLogLevel; +import static com.zaxxer.hikari.util.ClockSource.currentTime; +import static com.zaxxer.hikari.util.ClockSource.elapsedMillis; +import static com.zaxxer.hikari.util.UtilityElf.quietlySleep; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +import org.apache.logging.log4j.Level; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import com.zaxxer.hikari.mocks.StubConnection; +import com.zaxxer.hikari.util.UtilityElf; + +/** + * @author Brett Wooldridge + */ +public class ShutdownTest +{ + @Before + public void beforeTest() + { + setSlf4jLogLevel(PoolBase.class, Level.DEBUG); + setSlf4jLogLevel(HikariPool.class, Level.DEBUG); + StubConnection.count.set(0); + } + + @After + public void afterTest() + { + setSlf4jLogLevel(PoolBase.class, Level.WARN); + setSlf4jLogLevel(HikariPool.class, Level.WARN); + StubConnection.slowCreate = false; + } + + @Test + public void testShutdown1() throws SQLException + { + Assert.assertSame("StubConnection count not as expected", 0, StubConnection.count.get()); + + StubConnection.slowCreate = true; + + HikariConfig config = newHikariConfig(); + config.setMinimumIdle(0); + config.setMaximumPoolSize(10); + config.setInitializationFailTimeout(Long.MAX_VALUE); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + try (HikariDataSource ds = new HikariDataSource(config)) { + HikariPool pool = getPool(ds); + + Thread[] threads = new Thread[10]; + for (int i = 0; i < 10; i++) { + threads[i] = new Thread() { + @Override + public void run() + { + try { + if (ds.getConnection() != null) { + quietlySleep(SECONDS.toMillis(1)); + } + } + catch (SQLException e) { + } + } + }; + threads[i].setDaemon(true); + } + for (int i = 0; i < 10; i++) { + threads[i].start(); + } + + quietlySleep(1800L); + + assertTrue("Total connection count not as expected, ", pool.getTotalConnections() > 0); + + ds.close(); + + assertSame("Active connection count not as expected, ", 0, pool.getActiveConnections()); + assertSame("Idle connection count not as expected, ", 0, pool.getIdleConnections()); + assertSame("Total connection count not as expected, ", 0, pool.getTotalConnections()); + assertTrue(ds.isClosed()); + } + } + + @Test + public void testShutdown2() throws SQLException + { + assertSame("StubConnection count not as expected", 0, StubConnection.count.get()); + + StubConnection.slowCreate = true; + + HikariConfig config = newHikariConfig(); + config.setMinimumIdle(10); + config.setMaximumPoolSize(10); + config.setInitializationFailTimeout(0); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + try (HikariDataSource ds = new HikariDataSource(config)) { + HikariPool pool = getPool(ds); + + quietlySleep(1200L); + + assertTrue("Total connection count not as expected, ", pool.getTotalConnections() > 0); + + ds.close(); + + assertSame("Active connection count not as expected, ", 0, pool.getActiveConnections()); + assertSame("Idle connection count not as expected, ", 0, pool.getIdleConnections()); + assertSame("Total connection count not as expected, ", 0, pool.getTotalConnections()); + assertTrue(ds.toString().startsWith("HikariDataSource (") && ds.toString().endsWith(")")); + } + } + + @Test + public void testShutdown3() throws SQLException + { + assertSame("StubConnection count not as expected", 0, StubConnection.count.get()); + + StubConnection.slowCreate = false; + + HikariConfig config = newHikariConfig(); + config.setMinimumIdle(5); + config.setMaximumPoolSize(5); + config.setInitializationFailTimeout(0); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + try (HikariDataSource ds = new HikariDataSource(config)) { + HikariPool pool = getPool(ds); + + quietlySleep(1200L); + + assertTrue("Total connection count not as expected, ", pool.getTotalConnections() == 5); + + ds.close(); + + assertSame("Active connection count not as expected, ", 0, pool.getActiveConnections()); + assertSame("Idle connection count not as expected, ", 0, pool.getIdleConnections()); + assertSame("Total connection count not as expected, ", 0, pool.getTotalConnections()); + } + } + + @Test + public void testShutdown4() throws SQLException + { + StubConnection.slowCreate = true; + + HikariConfig config = newHikariConfig(); + config.setMinimumIdle(10); + config.setMaximumPoolSize(10); + config.setInitializationFailTimeout(Long.MAX_VALUE); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + try (HikariDataSource ds = new HikariDataSource(config)) { + quietlySleep(500L); + + ds.close(); + + long startTime = currentTime(); + while (elapsedMillis(startTime) < SECONDS.toMillis(5) && threadCount() > 0) { + quietlySleep(250); + } + + assertSame("Unreleased connections after shutdown", 0, getPool(ds).getTotalConnections()); + } + } + + @Test + public void testShutdown5() throws SQLException + { + Assert.assertSame("StubConnection count not as expected", 0, StubConnection.count.get()); + + HikariConfig config = newHikariConfig(); + config.setMinimumIdle(5); + config.setMaximumPoolSize(5); + config.setInitializationFailTimeout(0); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + try (HikariDataSource ds = new HikariDataSource(config)) { + HikariPool pool = getPool(ds); + + Connection[] connections = new Connection[5]; + for (int i = 0; i < 5; i++) { + connections[i] = ds.getConnection(); + } + + Assert.assertTrue("Total connection count not as expected, ", pool.getTotalConnections() == 5); + + ds.close(); + + Assert.assertSame("Active connection count not as expected, ", 0, pool.getActiveConnections()); + Assert.assertSame("Idle connection count not as expected, ", 0, pool.getIdleConnections()); + Assert.assertSame("Total connection count not as expected, ", 0, pool.getTotalConnections()); + } + } + + @Test + public void testAfterShutdown() throws Exception + { + HikariConfig config = newHikariConfig(); + config.setMinimumIdle(0); + config.setMaximumPoolSize(5); + config.setInitializationFailTimeout(0); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + try (HikariDataSource ds = new HikariDataSource(config)) { + ds.close(); + try { + ds.getConnection(); + } + catch (SQLException e) { + Assert.assertTrue(e.getMessage().contains("has been closed.")); + } + } + } + + @Test + public void testShutdownDuringInit() throws Exception + { + final HikariConfig config = newHikariConfig(); + config.setMinimumIdle(5); + config.setMaximumPoolSize(5); + config.setConnectionTimeout(1000); + config.setValidationTimeout(1000); + config.setInitializationFailTimeout(0); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + try (HikariDataSource ds = new HikariDataSource(config)) { + StubConnection.slowCreate = true; + UtilityElf.quietlySleep(3000L); + } + } + + @Test + public void testThreadedShutdown() throws Exception + { + final HikariConfig config = newHikariConfig(); + config.setMinimumIdle(5); + config.setMaximumPoolSize(5); + config.setConnectionTimeout(1000); + config.setValidationTimeout(1000); + config.setInitializationFailTimeout(0); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + for (int i = 0; i < 4; i++) { + try (final HikariDataSource ds = new HikariDataSource(config)) { + Thread t = new Thread() { + @Override + public void run() + { + try (Connection connection = ds.getConnection()) { + for (int i = 0; i < 10; i++) { + Connection connection2 = null; + try { + connection2 = ds.getConnection(); + PreparedStatement stmt = connection2.prepareStatement("SOMETHING"); + UtilityElf.quietlySleep(20); + stmt.getMaxFieldSize(); + } + catch (SQLException e) { + try { + if (connection2 != null) { + connection2.close(); + } + } + catch (SQLException e2) { + if (e2.getMessage().contains("shutdown") || e2.getMessage().contains("evicted")) { + break; + } + } + } + } + } + catch (Exception e) { + Assert.fail(e.getMessage()); + } + finally { + ds.close(); + } + } + }; + t.start(); + + Thread t2 = new Thread() { + @Override + public void run() + { + UtilityElf.quietlySleep(100); + try { + ds.close(); + } + catch (IllegalStateException e) { + Assert.fail(e.getMessage()); + } + } + }; + t2.start(); + + t.join(); + t2.join(); + + ds.close(); + } + } + } + + private int threadCount() + { + Thread[] threads = new Thread[Thread.activeCount() * 2]; + Thread.enumerate(threads); + + int count = 0; + for (Thread thread : threads) { + count += (thread != null && thread.getName().startsWith("Hikari")) ? 1 : 0; + } + + return count; + } +} diff --git a/src/test/java/com/zaxxer/hikari/pool/StatementTest.java b/src/test/java/com/zaxxer/hikari/pool/StatementTest.java new file mode 100644 index 0000000..ebe9c90 --- /dev/null +++ b/src/test/java/com/zaxxer/hikari/pool/StatementTest.java @@ -0,0 +1,119 @@ +/* + * 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.pool.TestElf.newHikariConfig; +import static com.zaxxer.hikari.pool.TestElf.getPool; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; + +public class StatementTest +{ + private HikariDataSource ds; + + @Before + public void setup() + { + HikariConfig config = newHikariConfig(); + config.setMinimumIdle(1); + config.setMaximumPoolSize(2); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + ds = new HikariDataSource(config); + } + + @After + public void teardown() + { + ds.close(); + } + + @Test + public void testStatementClose() throws SQLException + { + ds.getConnection().close(); + + HikariPool pool = getPool(ds); + assertTrue("Total connections not as expected", pool.getTotalConnections() >= 1); + assertTrue("Idle connections not as expected", pool.getIdleConnections() >= 1); + + try (Connection connection = ds.getConnection()) { + assertNotNull(connection); + + assertTrue("Total connections not as expected", pool.getTotalConnections() >= 1); + assertTrue("Idle connections not as expected", pool.getIdleConnections() >= 0); + + Statement statement = connection.createStatement(); + assertNotNull(statement); + + connection.close(); + + assertTrue(statement.isClosed()); + } + } + + @Test + public void testAutoStatementClose() throws SQLException + { + try (Connection connection = ds.getConnection()) { + assertNotNull(connection); + + Statement statement1 = connection.createStatement(); + assertNotNull(statement1); + Statement statement2 = connection.createStatement(); + assertNotNull(statement2); + + connection.close(); + + assertTrue(statement1.isClosed()); + assertTrue(statement2.isClosed()); + } + } + + @Test + public void testDoubleStatementClose() throws SQLException + { + try (Connection connection = ds.getConnection(); + Statement statement1 = connection.createStatement()) { + statement1.close(); + statement1.close(); + } + } + + @Test + public void testOutOfOrderStatementClose() throws SQLException + { + try (Connection connection = ds.getConnection(); + Statement statement1 = connection.createStatement(); + Statement statement2 = connection.createStatement()) { + statement1.close(); + statement2.close(); + } + } +} diff --git a/src/test/java/com/zaxxer/hikari/pool/TestConcurrentBag.java b/src/test/java/com/zaxxer/hikari/pool/TestConcurrentBag.java new file mode 100644 index 0000000..b8d9d22 --- /dev/null +++ b/src/test/java/com/zaxxer/hikari/pool/TestConcurrentBag.java @@ -0,0 +1,117 @@ +/* + * 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 static com.zaxxer.hikari.pool.TestElf.getPool; +import static com.zaxxer.hikari.pool.TestElf.newHikariConfig; +import static com.zaxxer.hikari.pool.TestElf.setSlf4jTargetStream; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.util.concurrent.CompletableFuture; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import com.zaxxer.hikari.util.ConcurrentBag; + +/** + * + * @author Brett Wooldridge + */ +public class TestConcurrentBag +{ + private static HikariDataSource ds; + private static HikariPool pool; + + @BeforeClass + public static void setup() + { + HikariConfig config = newHikariConfig(); + config.setMinimumIdle(1); + config.setMaximumPoolSize(2); + config.setInitializationFailTimeout(0); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + ds = new HikariDataSource(config); + pool = getPool(ds); + } + + @AfterClass + public static void teardown() + { + ds.close(); + } + + @Test + public void testConcurrentBag() throws Exception + { + try (ConcurrentBag<PoolEntry> bag = new ConcurrentBag<>((x) -> CompletableFuture.completedFuture(Boolean.TRUE))) { + assertEquals(0, bag.values(8).size()); + + PoolEntry reserved = pool.newPoolEntry(); + bag.add(reserved); + bag.reserve(reserved); // reserved + + PoolEntry inuse = pool.newPoolEntry(); + bag.add(inuse); + bag.borrow(2, MILLISECONDS); // in use + + PoolEntry notinuse = pool.newPoolEntry(); + bag.add(notinuse); // not in use + + bag.dumpState(); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + PrintStream ps = new PrintStream(baos, true); + setSlf4jTargetStream(ConcurrentBag.class, ps); + + bag.requite(reserved); + + bag.remove(notinuse); + assertTrue(new String(baos.toByteArray()).contains("not borrowed or reserved")); + + bag.unreserve(notinuse); + assertTrue(new String(baos.toByteArray()).contains("was not reserved")); + + bag.remove(inuse); + bag.remove(inuse); + assertTrue(new String(baos.toByteArray()).contains("not borrowed or reserved")); + + bag.close(); + try { + PoolEntry bagEntry = pool.newPoolEntry(); + bag.add(bagEntry); + assertNotEquals(bagEntry, bag.borrow(100, MILLISECONDS)); + } + catch (IllegalStateException e) { + assertTrue(new String(baos.toByteArray()).contains("ignoring add()")); + } + + assertNotNull(notinuse.toString()); + } + } +} diff --git a/src/test/java/com/zaxxer/hikari/pool/TestConnectionCloseBlocking.java b/src/test/java/com/zaxxer/hikari/pool/TestConnectionCloseBlocking.java new file mode 100644 index 0000000..8d98f83 --- /dev/null +++ b/src/test/java/com/zaxxer/hikari/pool/TestConnectionCloseBlocking.java @@ -0,0 +1,98 @@ +/* + * 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.pool.TestElf.newHikariConfig; +import static com.zaxxer.hikari.util.ClockSource.currentTime; +import static com.zaxxer.hikari.util.ClockSource.elapsedMillis; +import static com.zaxxer.hikari.util.UtilityElf.quietlySleep; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.when; + +import java.sql.Connection; +import java.sql.SQLException; + +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import com.zaxxer.hikari.mocks.MockDataSource; + +/** + * Test for cases when db network connectivity goes down and close is called on existing connections. By default Hikari + * blocks longer than getMaximumTimeout (it can hang for a lot of time depending on driver timeout settings). Closing + * async the connections fixes this issue. + * + */ +public class TestConnectionCloseBlocking { + private static volatile boolean shouldFail = false; + + // @Test + public void testConnectionCloseBlocking() throws SQLException { + HikariConfig config = newHikariConfig(); + config.setMinimumIdle(0); + config.setMaximumPoolSize(1); + config.setConnectionTimeout(1500); + config.setDataSource(new CustomMockDataSource()); + + long start = currentTime(); + try (HikariDataSource ds = new HikariDataSource(config); + Connection connection = ds.getConnection()) { + + connection.close(); + + // Hikari only checks for validity for connections with lastAccess > 1000 ms so we sleep for 1001 ms to force + // Hikari to do a connection validation which will fail and will trigger the connection to be closed + quietlySleep(1100L); + + shouldFail = true; + + // on physical connection close we sleep 2 seconds + try (Connection connection2 = ds.getConnection()) { + assertTrue("Waited longer than timeout", (elapsedMillis(start) < config.getConnectionTimeout())); + } + } catch (SQLException e) { + assertTrue("getConnection failed because close connection took longer than timeout", (elapsedMillis(start) < config.getConnectionTimeout())); + } + } + + private static class CustomMockDataSource extends MockDataSource { + @Override + public Connection getConnection() throws SQLException { + Connection mockConnection = super.getConnection(); + when(mockConnection.isValid(anyInt())).thenReturn(!shouldFail); + doAnswer(new Answer<Void>() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + if (shouldFail) { + SECONDS.sleep(2); + } + return null; + } + }).when(mockConnection).close(); + return mockConnection; + } + } + +} diff --git a/src/test/java/com/zaxxer/hikari/pool/TestConnectionTimeoutRetry.java b/src/test/java/com/zaxxer/hikari/pool/TestConnectionTimeoutRetry.java new file mode 100644 index 0000000..15f8974 --- /dev/null +++ b/src/test/java/com/zaxxer/hikari/pool/TestConnectionTimeoutRetry.java @@ -0,0 +1,277 @@ +/* + * 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.pool.TestElf.getPool; +import static com.zaxxer.hikari.pool.TestElf.newHikariConfig; +import static com.zaxxer.hikari.pool.TestElf.setSlf4jLogLevel; +import static com.zaxxer.hikari.pool.TestElf.setSlf4jTargetStream; +import static com.zaxxer.hikari.util.ClockSource.currentTime; +import static com.zaxxer.hikari.util.ClockSource.elapsedMillis; +import static java.lang.Thread.sleep; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import org.apache.logging.log4j.Level; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import com.zaxxer.hikari.mocks.StubConnection; +import com.zaxxer.hikari.mocks.StubDataSource; + +public class TestConnectionTimeoutRetry +{ + @Test + public void testConnectionRetries() throws SQLException + { + HikariConfig config = newHikariConfig(); + config.setMinimumIdle(0); + config.setMaximumPoolSize(1); + config.setConnectionTimeout(2800); + config.setValidationTimeout(2800); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + try (HikariDataSource ds = new HikariDataSource(config)) { + StubDataSource stubDataSource = ds.unwrap(StubDataSource.class); + stubDataSource.setThrowException(new SQLException("Connection refused")); + + long start = currentTime(); + try (Connection connection = ds.getConnection()) { + connection.close(); + fail("Should not have been able to get a connection."); + } + catch (SQLException e) { + long elapsed = elapsedMillis(start); + long timeout = config.getConnectionTimeout(); + assertTrue("Didn't wait long enough for timeout", (elapsed >= timeout)); + } + } + } + + @Test + public void testConnectionRetries2() throws SQLException + { + HikariConfig config = newHikariConfig(); + config.setMinimumIdle(0); + config.setMaximumPoolSize(1); + config.setConnectionTimeout(2800); + config.setValidationTimeout(2800); + config.setInitializationFailTimeout(0); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + try (HikariDataSource ds = new HikariDataSource(config)) { + final StubDataSource stubDataSource = ds.unwrap(StubDataSource.class); + stubDataSource.setThrowException(new SQLException("Connection refused")); + + ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); + scheduler.schedule(new Runnable() { + @Override + public void run() + { + stubDataSource.setThrowException(null); + } + }, 300, TimeUnit.MILLISECONDS); + + long start = currentTime(); + try { + try (Connection connection = ds.getConnection()) { + // close immediately + } + + long elapsed = elapsedMillis(start); + assertTrue("Connection returned too quickly, something is wrong.", elapsed > 250); + assertTrue("Waited too long to get a connection.", elapsed < config.getConnectionTimeout()); + } + catch (SQLException e) { + fail("Should not have timed out: " + e.getMessage()); + } + finally { + scheduler.shutdownNow(); + } + } + } + + @Test + public void testConnectionRetries3() throws SQLException + { + HikariConfig config = newHikariConfig(); + config.setMinimumIdle(0); + config.setMaximumPoolSize(2); + config.setConnectionTimeout(2800); + config.setValidationTimeout(2800); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + try (HikariDataSource ds = new HikariDataSource(config)) { + final Connection connection1 = ds.getConnection(); + final Connection connection2 = ds.getConnection(); + assertNotNull(connection1); + assertNotNull(connection2); + + ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2); + scheduler.schedule(new Runnable() { + @Override + public void run() + { + try { + connection1.close(); + } + catch (Exception e) { + e.printStackTrace(System.err); + } + } + }, 800, MILLISECONDS); + + long start = currentTime(); + try { + try (Connection connection3 = ds.getConnection()) { + // close immediately + } + + long elapsed = elapsedMillis(start); + assertTrue("Waited too long to get a connection.", (elapsed >= 700) && (elapsed < 950)); + } + catch (SQLException e) { + fail("Should not have timed out."); + } + finally { + scheduler.shutdownNow(); + } + } + } + + @Test + public void testConnectionRetries5() throws SQLException + { + HikariConfig config = newHikariConfig(); + config.setMinimumIdle(0); + config.setMaximumPoolSize(2); + config.setConnectionTimeout(1000); + config.setValidationTimeout(1000); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + try (HikariDataSource ds = new HikariDataSource(config)) { + final Connection connection1 = ds.getConnection(); + + long start = currentTime(); + + ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2); + scheduler.schedule(new Runnable() { + @Override + public void run() + { + try { + connection1.close(); + } + catch (Exception e) { + e.printStackTrace(System.err); + } + } + }, 250, MILLISECONDS); + + StubDataSource stubDataSource = ds.unwrap(StubDataSource.class); + stubDataSource.setThrowException(new SQLException("Connection refused")); + + try { + try (Connection connection2 = ds.getConnection()) { + // close immediately + } + + long elapsed = elapsedMillis(start); + assertTrue("Waited too long to get a connection.", (elapsed >= 250) && (elapsed < config.getConnectionTimeout())); + } + catch (SQLException e) { + fail("Should not have timed out."); + } + finally { + scheduler.shutdownNow(); + } + } + } + + @Test + public void testConnectionIdleFill() throws Exception + { + StubConnection.slowCreate = false; + + HikariConfig config = newHikariConfig(); + config.setMinimumIdle(5); + config.setMaximumPoolSize(10); + config.setConnectionTimeout(2000); + config.setValidationTimeout(2000); + config.setConnectionTestQuery("VALUES 2"); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + System.setProperty("com.zaxxer.hikari.housekeeping.periodMs", "400"); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + PrintStream ps = new PrintStream(baos, true); + setSlf4jTargetStream(HikariPool.class, ps); + + try (HikariDataSource ds = new HikariDataSource(config)) { + setSlf4jLogLevel(HikariPool.class, Level.DEBUG); + + HikariPool pool = getPool(ds); + try ( + Connection connection1 = ds.getConnection(); + Connection connection2 = ds.getConnection(); + Connection connection3 = ds.getConnection(); + Connection connection4 = ds.getConnection(); + Connection connection5 = ds.getConnection(); + Connection connection6 = ds.getConnection(); + Connection connection7 = ds.getConnection()) { + + sleep(1300); + + assertSame("Total connections not as expected", 10, pool.getTotalConnections()); + assertSame("Idle connections not as expected", 3, pool.getIdleConnections()); + } + + assertSame("Total connections not as expected", 10, pool.getTotalConnections()); + assertSame("Idle connections not as expected", 10, pool.getIdleConnections()); + } + } + + @Before + public void before() + { + setSlf4jLogLevel(HikariPool.class, Level.INFO); + } + + @After + public void after() + { + System.getProperties().remove("com.zaxxer.hikari.housekeeping.periodMs"); + } +} diff --git a/src/test/java/com/zaxxer/hikari/pool/TestConnections.java b/src/test/java/com/zaxxer/hikari/pool/TestConnections.java new file mode 100644 index 0000000..538d9ac --- /dev/null +++ b/src/test/java/com/zaxxer/hikari/pool/TestConnections.java @@ -0,0 +1,621 @@ +/* + * 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 static com.zaxxer.hikari.pool.TestElf.newHikariConfig; +import static com.zaxxer.hikari.pool.TestElf.newHikariDataSource; +import static com.zaxxer.hikari.pool.TestElf.getPool; +import static com.zaxxer.hikari.pool.TestElf.setConfigUnitTest; +import static com.zaxxer.hikari.pool.TestElf.setSlf4jLogLevel; +import static com.zaxxer.hikari.pool.TestElf.setSlf4jTargetStream; +import static com.zaxxer.hikari.util.UtilityElf.quietlySleep; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.SQLTransientConnectionException; +import java.sql.Statement; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.logging.log4j.Level; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import com.zaxxer.hikari.mocks.StubConnection; +import com.zaxxer.hikari.mocks.StubDataSource; +import com.zaxxer.hikari.mocks.StubStatement; +import com.zaxxer.hikari.pool.HikariPool.PoolInitializationException; + +/** + * @author Brett Wooldridge + */ +public class TestConnections +{ + @Before + public void before() + { + setSlf4jTargetStream(HikariPool.class, System.err); + setSlf4jLogLevel(HikariPool.class, Level.DEBUG); + setSlf4jLogLevel(PoolBase.class, Level.DEBUG); + } + + @After + public void after() + { + System.getProperties().remove("com.zaxxer.hikari.housekeeping.periodMs"); + setSlf4jLogLevel(HikariPool.class, Level.WARN); + setSlf4jLogLevel(PoolBase.class, Level.WARN); + } + + @Test + public void testCreate() throws SQLException + { + HikariConfig config = newHikariConfig(); + config.setMinimumIdle(1); + config.setMaximumPoolSize(1); + config.setConnectionTestQuery("VALUES 1"); + config.setConnectionInitSql("SELECT 1"); + config.setReadOnly(true); + config.setConnectionTimeout(2500); + config.setLeakDetectionThreshold(TimeUnit.SECONDS.toMillis(30)); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + try (HikariDataSource ds = new HikariDataSource(config)) { + ds.setLoginTimeout(10); + assertSame(10, ds.getLoginTimeout()); + + HikariPool pool = getPool(ds); + ds.getConnection().close(); + assertSame("Total connections not as expected", 1, pool.getTotalConnections()); + assertSame("Idle connections not as expected", 1, pool.getIdleConnections()); + + try (Connection connection = ds.getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT * FROM device WHERE device_id=?")) { + + assertNotNull(connection); + assertNotNull(statement); + + assertSame("Total connections not as expected", 1, pool.getTotalConnections()); + assertSame("Idle connections not as expected", 0, pool.getIdleConnections()); + + statement.setInt(1, 0); + + try (ResultSet resultSet = statement.executeQuery()) { + assertNotNull(resultSet); + + assertFalse(resultSet.next()); + } + } + + assertSame("Total connections not as expected", 1, pool.getTotalConnections()); + assertSame("Idle connections not as expected", 1, pool.getIdleConnections()); + } + } + + @Test + public void testMaxLifetime() throws Exception + { + HikariConfig config = newHikariConfig(); + config.setMinimumIdle(0); + config.setMaximumPoolSize(1); + config.setConnectionTimeout(2500); + config.setConnectionTestQuery("VALUES 1"); + config.setInitializationFailTimeout(Long.MAX_VALUE); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + System.setProperty("com.zaxxer.hikari.housekeeping.periodMs", "100"); + + setConfigUnitTest(true); + try (HikariDataSource ds = new HikariDataSource(config)) { + System.clearProperty("com.zaxxer.hikari.housekeeping.periodMs"); + + ds.setMaxLifetime(700); + + HikariPool pool = getPool(ds); + + assertSame("Total connections not as expected", 0, pool.getTotalConnections()); + assertSame("Idle connections not as expected", 0, pool.getIdleConnections()); + + Connection unwrap; + Connection unwrap2; + try (Connection connection = ds.getConnection()) { + unwrap = connection.unwrap(Connection.class); + assertNotNull(connection); + + assertSame("Second total connections not as expected", 1, pool.getTotalConnections()); + assertSame("Second idle connections not as expected", 0, pool.getIdleConnections()); + } + + assertSame("Idle connections not as expected", 1, pool.getIdleConnections()); + + try (Connection connection = ds.getConnection()) { + unwrap2 = connection.unwrap(Connection.class); + assertSame(unwrap, unwrap2); + assertSame("Second total connections not as expected", 1, pool.getTotalConnections()); + assertSame("Second idle connections not as expected", 0, pool.getIdleConnections()); + } + + quietlySleep(TimeUnit.SECONDS.toMillis(2)); + + try (Connection connection = ds.getConnection()) { + unwrap2 = connection.unwrap(Connection.class); + assertNotSame("Expected a different connection", unwrap, unwrap2); + } + + assertSame("Post total connections not as expected", 1, pool.getTotalConnections()); + assertSame("Post idle connections not as expected", 1, pool.getIdleConnections()); + } + finally { + setConfigUnitTest(false); + } + } + + @Test + public void testMaxLifetime2() throws Exception + { + HikariConfig config = newHikariConfig(); + config.setMinimumIdle(0); + config.setMaximumPoolSize(1); + config.setConnectionTimeout(2500); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + System.setProperty("com.zaxxer.hikari.housekeeping.periodMs", "100"); + + setConfigUnitTest(true); + try (HikariDataSource ds = new HikariDataSource(config)) { + ds.setMaxLifetime(700); + + HikariPool pool = getPool(ds); + assertSame("Total connections not as expected", 0, pool.getTotalConnections()); + assertSame("Idle connections not as expected", 0, pool.getIdleConnections()); + + Connection unwrap; + Connection unwrap2; + try (Connection connection = ds.getConnection()) { + unwrap = connection.unwrap(Connection.class); + assertNotNull(connection); + + assertSame("Second total connections not as expected", 1, pool.getTotalConnections()); + assertSame("Second idle connections not as expected", 0, pool.getIdleConnections()); + } + + assertSame("Idle connections not as expected", 1, pool.getIdleConnections()); + + try (Connection connection = ds.getConnection()) { + unwrap2 = connection.unwrap(Connection.class); + assertSame(unwrap, unwrap2); + assertSame("Second total connections not as expected", 1, pool.getTotalConnections()); + assertSame("Second idle connections not as expected", 0, pool.getIdleConnections()); + } + + quietlySleep(800); + + try (Connection connection = ds.getConnection()) { + unwrap2 = connection.unwrap(Connection.class); + assertNotSame("Expected a different connection", unwrap, unwrap2); + } + + assertSame("Post total connections not as expected", 1, pool.getTotalConnections()); + assertSame("Post idle connections not as expected", 1, pool.getIdleConnections()); + } + finally { + setConfigUnitTest(false); + } + } + + @Test + public void testDoubleClose() throws Exception + { + HikariConfig config = newHikariConfig(); + config.setMinimumIdle(1); + config.setMaximumPoolSize(1); + config.setConnectionTimeout(2500); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + try (HikariDataSource ds = new HikariDataSource(config); + Connection connection = ds.getConnection()) { + connection.close(); + + // should no-op + connection.abort(null); + + assertTrue("Connection should have closed", connection.isClosed()); + assertFalse("Connection should have closed", connection.isValid(5)); + assertTrue("Expected to contain ClosedConnection, but was " + connection, connection.toString().contains("ClosedConnection")); + } + } + + @Test + public void testEviction() throws Exception + { + HikariConfig config = newHikariConfig(); + config.setMinimumIdle(0); + config.setMaximumPoolSize(5); + config.setConnectionTimeout(2500); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + try (HikariDataSource ds = new HikariDataSource(config)) { + Connection connection = ds.getConnection(); + + HikariPool pool = getPool(ds); + assertEquals(1, pool.getTotalConnections()); + ds.evictConnection(connection); + assertEquals(0, pool.getTotalConnections()); + } + } + + @Test + public void testBackfill() throws Exception + { + HikariConfig config = newHikariConfig(); + config.setMinimumIdle(1); + config.setMaximumPoolSize(4); + config.setConnectionTimeout(1000); + config.setInitializationFailTimeout(Long.MAX_VALUE); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + StubConnection.slowCreate = true; + try (HikariDataSource ds = new HikariDataSource(config)) { + + HikariPool pool = getPool(ds); + quietlySleep(1250); + + assertSame("Total connections not as expected", 1, pool.getTotalConnections()); + assertSame("Idle connections not as expected", 1, pool.getIdleConnections()); + + // This will take the pool down to zero + try (Connection connection = ds.getConnection()) { + assertNotNull(connection); + + assertSame("Total connections not as expected", 1, pool.getTotalConnections()); + assertSame("Idle connections not as expected", 0, pool.getIdleConnections()); + + PreparedStatement statement = connection.prepareStatement("SELECT some, thing FROM somewhere WHERE something=?"); + assertNotNull(statement); + + ResultSet resultSet = statement.executeQuery(); + assertNotNull(resultSet); + + try { + statement.getMaxFieldSize(); + fail(); + } + catch (Exception e) { + assertSame(SQLException.class, e.getClass()); + } + + pool.logPoolState("testBackfill() before close..."); + + // The connection will be ejected from the pool here + } + + assertSame("Total connections not as expected", 0, pool.getTotalConnections()); + + pool.logPoolState("testBackfill() after close..."); + + quietlySleep(1250); + + assertSame("Total connections not as expected", 1, pool.getTotalConnections()); + assertSame("Idle connections not as expected", 1, pool.getIdleConnections()); + } + finally { + StubConnection.slowCreate = false; + } + } + + @Test + public void testMaximumPoolLimit() throws Exception + { + HikariConfig config = newHikariConfig(); + config.setMinimumIdle(1); + config.setMaximumPoolSize(4); + config.setConnectionTimeout(20000); + config.setInitializationFailTimeout(0); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + final AtomicReference<Exception> ref = new AtomicReference<>(); + + StubConnection.count.set(0); // reset counter + + try (final HikariDataSource ds = new HikariDataSource(config)) { + + final HikariPool pool = getPool(ds); + + Thread[] threads = new Thread[20]; + for (int i = 0; i < threads.length; i++) { + threads[i] = new Thread(new Runnable() { + @Override + public void run() + { + try { + pool.logPoolState("Before acquire "); + try (Connection connection = ds.getConnection()) { + pool.logPoolState("After acquire "); + quietlySleep(500); + } + } + catch (Exception e) { + ref.set(e); + } + } + }); + } + + for (int i = 0; i < threads.length; i++) { + threads[i].start(); + } + + for (int i = 0; i < threads.length; i++) { + threads[i].join(); + } + + pool.logPoolState("before check "); + assertNull((ref.get() != null ? ref.get().toString() : ""), ref.get()); + assertSame("StubConnection count not as expected", 4, StubConnection.count.get()); + } + } + + @Test + public void testOldDriver() throws Exception + { + HikariConfig config = newHikariConfig(); + config.setMinimumIdle(1); + config.setMaximumPoolSize(1); + config.setConnectionTimeout(2500); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + StubConnection.oldDriver = true; + StubStatement.oldDriver = true; + try (HikariDataSource ds = new HikariDataSource(config)) { + quietlySleep(500); + + try (Connection connection = ds.getConnection()) { + // close + } + + quietlySleep(500); + try (Connection connection = ds.getConnection()) { + // close + } + } + finally { + StubConnection.oldDriver = false; + StubStatement.oldDriver = false; + } + } + + @Test + public void testSuspendResume() throws Exception + { + HikariConfig config = newHikariConfig(); + config.setMinimumIdle(3); + config.setMaximumPoolSize(3); + config.setConnectionTimeout(2500); + config.setAllowPoolSuspension(true); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + try (final HikariDataSource ds = new HikariDataSource(config)) { + HikariPool pool = getPool(ds); + while (pool.getTotalConnections() < 3) { + quietlySleep(50); + } + + Thread t = new Thread(new Runnable() { + @Override + public void run() + { + try { + ds.getConnection(); + ds.getConnection(); + } + catch (Exception e) { + fail(); + } + } + }); + + try (Connection c3 = ds.getConnection()) { + assertEquals(2, pool.getIdleConnections()); + + pool.suspendPool(); + t.start(); + + quietlySleep(500); + assertEquals(2, pool.getIdleConnections()); + } + assertEquals(3, pool.getIdleConnections()); + pool.resumePool(); + quietlySleep(500); + assertEquals(1, pool.getIdleConnections()); + } + } + + @Test + public void testInitializationFailure1() throws SQLException + { + StubDataSource stubDataSource = new StubDataSource(); + stubDataSource.setThrowException(new SQLException("Connection refused")); + + try (HikariDataSource ds = newHikariDataSource()) { + ds.setMinimumIdle(1); + ds.setMaximumPoolSize(1); + ds.setConnectionTimeout(2500); + ds.setConnectionTestQuery("VALUES 1"); + ds.setDataSource(stubDataSource); + + try (Connection c = ds.getConnection()) { + fail("Initialization should have failed"); + } + catch (SQLException e) { + // passed + } + } + } + + @Test + public void testInitializationFailure2() throws SQLException + { + StubDataSource stubDataSource = new StubDataSource(); + stubDataSource.setThrowException(new SQLException("Connection refused")); + + HikariConfig config = newHikariConfig(); + config.setMinimumIdle(1); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSource(stubDataSource); + + try (HikariDataSource ds = new HikariDataSource(config); + Connection c = ds.getConnection()) { + fail("Initialization should have failed"); + } + catch (PoolInitializationException e) { + // passed + } + } + + @Test + public void testInvalidConnectionTestQuery() + { + class BadConnection extends StubConnection { + /** {@inheritDoc} */ + @Override + public Statement createStatement() throws SQLException + { + throw new SQLException("Simulated exception in createStatement()"); + } + } + + StubDataSource stubDataSource = new StubDataSource() { + /** {@inheritDoc} */ + @Override + public Connection getConnection() throws SQLException + { + return new BadConnection(); + } + }; + + HikariConfig config = newHikariConfig(); + config.setMinimumIdle(1); + config.setMaximumPoolSize(2); + config.setConnectionTimeout(TimeUnit.SECONDS.toMillis(3)); + config.setConnectionTestQuery("VALUES 1"); + config.setInitializationFailTimeout(TimeUnit.SECONDS.toMillis(2)); + config.setDataSource(stubDataSource); + + try (HikariDataSource ds = new HikariDataSource(config)) { + try (Connection c = ds.getConnection()) { + fail("getConnection() should have failed"); + } + catch (SQLException e) { + assertSame("Simulated exception in createStatement()", e.getNextException().getMessage()); + } + } + catch (PoolInitializationException e) { + assertSame("Simulated exception in createStatement()", e.getCause().getMessage()); + } + + config.setInitializationFailTimeout(0); + try (HikariDataSource ds = new HikariDataSource(config)) { + fail("Initialization should have failed"); + } + catch (PoolInitializationException e) { + // passed + } + } + + @Test + public void testPopulationSlowAcquisition() throws InterruptedException, SQLException + { + HikariConfig config = newHikariConfig(); + config.setMaximumPoolSize(20); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + System.setProperty("com.zaxxer.hikari.housekeeping.periodMs", "1000"); + + StubConnection.slowCreate = true; + try (HikariDataSource ds = new HikariDataSource(config)) { + System.clearProperty("com.zaxxer.hikari.housekeeping.periodMs"); + + ds.setIdleTimeout(3000); + + SECONDS.sleep(2); + + HikariPool pool = getPool(ds); + assertSame("Total connections not as expected", 2, pool.getTotalConnections()); + assertSame("Idle connections not as expected", 2, pool.getIdleConnections()); + + try (Connection connection = ds.getConnection()) { + assertNotNull(connection); + + SECONDS.sleep(20); + + assertSame("Second total connections not as expected", 20, pool.getTotalConnections()); + assertSame("Second idle connections not as expected", 19, pool.getIdleConnections()); + } + + assertSame("Idle connections not as expected", 20, pool.getIdleConnections()); + + SECONDS.sleep(5); + + assertSame("Third total connections not as expected", 20, pool.getTotalConnections()); + assertSame("Third idle connections not as expected", 20, pool.getIdleConnections()); + } + finally { + StubConnection.slowCreate = false; + } + } + + @Test + public void testMinimumIdleZero() throws SQLException + { + HikariConfig config = newHikariConfig(); + config.setMinimumIdle(0); + config.setMaximumPoolSize(5); + config.setConnectionTimeout(1000L); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + try (HikariDataSource ds = new HikariDataSource(config); + Connection connection = ds.getConnection()) { + // passed + } + catch (SQLTransientConnectionException sqle) { + fail("Failed to obtain connection"); + } + } +} diff --git a/src/test/java/com/zaxxer/hikari/pool/TestElf.java b/src/test/java/com/zaxxer/hikari/pool/TestElf.java new file mode 100644 index 0000000..6438b11 --- /dev/null +++ b/src/test/java/com/zaxxer/hikari/pool/TestElf.java @@ -0,0 +1,177 @@ +/* + * 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.io.PrintStream; +import java.lang.reflect.Field; +import java.sql.Connection; +import java.util.HashMap; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.core.Appender; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.Logger; +import org.apache.logging.log4j.core.appender.AbstractAppender; +import org.apache.logging.log4j.core.layout.CsvLogEventLayout; +import org.apache.logging.slf4j.Log4jLogger; +import org.slf4j.LoggerFactory; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; + +/** + * Utility methods for testing. + * + * @author Brett Wooldridge + */ +public final class TestElf +{ + private TestElf() { + // default constructor + } + + public static HikariPool getPool(HikariDataSource ds) + { + try { + Field field = ds.getClass().getDeclaredField("pool"); + field.setAccessible(true); + return (HikariPool) field.get(ds); + } + catch (Exception e) { + throw new RuntimeException(e); + } + } + + @SuppressWarnings("unchecked") + public static HashMap<Object, HikariPool> getMultiPool(HikariDataSource ds) + { + try { + Field field = ds.getClass().getDeclaredField("multiPool"); + field.setAccessible(true); + return (HashMap<Object, HikariPool>) field.get(ds); + } + catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static boolean getConnectionCommitDirtyState(Connection connection) + { + try { + Field field = ProxyConnection.class.getDeclaredField("isCommitStateDirty"); + field.setAccessible(true); + return field.getBoolean(connection); + } + catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static void setConfigUnitTest(boolean unitTest) + { + try { + Field field = HikariConfig.class.getDeclaredField("unitTest"); + field.setAccessible(true); + field.setBoolean(null, unitTest); + } + catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static void setSlf4jTargetStream(Class<?> clazz, PrintStream stream) + { + try { + Log4jLogger log4Jlogger = (Log4jLogger) LoggerFactory.getLogger(clazz); + + Field field = clazz.getClassLoader().loadClass("org.apache.logging.slf4j.Log4jLogger").getDeclaredField("logger"); + field.setAccessible(true); + + Logger logger = (Logger) field.get(log4Jlogger); + if (logger.getAppenders().containsKey("string")) { + Appender appender = logger.getAppenders().get("string"); + logger.removeAppender(appender); + } + + logger.addAppender(new StringAppender("string", stream)); + } + catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static void setSlf4jLogLevel(Class<?> clazz, Level logLevel) + { + try { + Log4jLogger log4Jlogger = (Log4jLogger) LoggerFactory.getLogger(clazz); + + Field field = clazz.getClassLoader().loadClass("org.apache.logging.slf4j.Log4jLogger").getDeclaredField("logger"); + field.setAccessible(true); + + Logger logger = (Logger) field.get(log4Jlogger); + logger.setLevel(logLevel); + } + catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static HikariConfig newHikariConfig() + { + final StackTraceElement callerStackTrace = Thread.currentThread().getStackTrace()[2]; + + String poolName = callerStackTrace.getMethodName(); + if ("setup".equals(poolName)) { + poolName = callerStackTrace.getClassName(); + } + + final HikariConfig config = new HikariConfig(); + config.setPoolName(poolName); + return config; + } + + public static HikariDataSource newHikariDataSource() + { + final StackTraceElement callerStackTrace = Thread.currentThread().getStackTrace()[2]; + + String poolName = callerStackTrace.getMethodName(); + if ("setup".equals(poolName)) { + poolName = callerStackTrace.getClassName(); + } + + final HikariDataSource ds = new HikariDataSource(); + ds.setPoolName(poolName); + return ds; + } + + private static class StringAppender extends AbstractAppender + { + private PrintStream stream; + + StringAppender(String name, PrintStream stream) + { + super(name, null, CsvLogEventLayout.createDefaultLayout()); + this.stream = stream; + } + + @Override + public void append(LogEvent event) + { + stream.println(event.getMessage().getFormattedMessage()); + } + } +} diff --git a/src/test/java/com/zaxxer/hikari/pool/TestHibernate.java b/src/test/java/com/zaxxer/hikari/pool/TestHibernate.java new file mode 100644 index 0000000..c1d6bae --- /dev/null +++ b/src/test/java/com/zaxxer/hikari/pool/TestHibernate.java @@ -0,0 +1,57 @@ +/* + * 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 org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +import java.sql.Connection; +import java.util.Properties; + +import org.hibernate.service.UnknownUnwrapTypeException; +import org.junit.Test; + +import com.zaxxer.hikari.hibernate.HikariConnectionProvider; + +public class TestHibernate +{ + @Test + public void testConnectionProvider() throws Exception + { + HikariConnectionProvider provider = new HikariConnectionProvider(); + + Properties props = new Properties(); + props.load(getClass().getResourceAsStream("/hibernate.properties")); + + provider.configure(props); + Connection connection = provider.getConnection(); + provider.closeConnection(connection); + + assertNotNull(provider.unwrap(HikariConnectionProvider.class)); + assertFalse(provider.supportsAggressiveRelease()); + + try { + provider.unwrap(TestHibernate.class); + fail("Expected exception"); + } + catch (UnknownUnwrapTypeException e) { + } + + provider.stop(); + } +} diff --git a/src/test/java/com/zaxxer/hikari/pool/TestJNDI.java b/src/test/java/com/zaxxer/hikari/pool/TestJNDI.java new file mode 100644 index 0000000..b5cd40c --- /dev/null +++ b/src/test/java/com/zaxxer/hikari/pool/TestJNDI.java @@ -0,0 +1,146 @@ +/* + * 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 org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import javax.naming.Context; +import javax.naming.Name; +import javax.naming.NamingException; +import javax.naming.RefAddr; +import javax.naming.Reference; + +import org.junit.Test; +import org.osjava.sj.jndi.AbstractContext; + +import com.zaxxer.hikari.HikariDataSource; +import com.zaxxer.hikari.HikariJNDIFactory; +import com.zaxxer.hikari.mocks.StubDataSource; + +public class TestJNDI +{ + @Test + public void testJndiLookup1() throws Exception + { + HikariJNDIFactory jndi = new HikariJNDIFactory(); + Reference ref = new Reference("javax.sql.DataSource"); + ref.add(new BogusRef("driverClassName", "com.zaxxer.hikari.mocks.StubDriver")); + ref.add(new BogusRef("jdbcUrl", "jdbc:stub")); + ref.add(new BogusRef("username", "foo")); + ref.add(new BogusRef("password", "foo")); + ref.add(new BogusRef("minimumIdle", "0")); + ref.add(new BogusRef("maxLifetime", "30000")); + ref.add(new BogusRef("maximumPoolSize", "10")); + ref.add(new BogusRef("dataSource.loginTimeout", "10")); + Context nameCtx = new BogusContext(); + + try (HikariDataSource ds = (HikariDataSource) jndi.getObjectInstance(ref, null, nameCtx, null)) { + assertNotNull(ds); + assertEquals("foo", ds.getUsername()); + } + } + + @Test + public void testJndiLookup2() throws Exception + { + HikariJNDIFactory jndi = new HikariJNDIFactory(); + Reference ref = new Reference("javax.sql.DataSource"); + ref.add(new BogusRef("dataSourceJNDI", "java:comp/env/HikariDS")); + ref.add(new BogusRef("driverClassName", "com.zaxxer.hikari.mocks.StubDriver")); + ref.add(new BogusRef("jdbcUrl", "jdbc:stub")); + ref.add(new BogusRef("username", "foo")); + ref.add(new BogusRef("password", "foo")); + ref.add(new BogusRef("minimumIdle", "0")); + ref.add(new BogusRef("maxLifetime", "30000")); + ref.add(new BogusRef("maximumPoolSize", "10")); + ref.add(new BogusRef("dataSource.loginTimeout", "10")); + Context nameCtx = new BogusContext2(); + + try (HikariDataSource ds = (HikariDataSource) jndi.getObjectInstance(ref, null, nameCtx, null)) { + assertNotNull(ds); + assertEquals("foo", ds.getUsername()); + } + } + + @Test + public void testJndiLookup3() throws Exception + { + HikariJNDIFactory jndi = new HikariJNDIFactory(); + + Reference ref = new Reference("javax.sql.DataSource"); + ref.add(new BogusRef("dataSourceJNDI", "java:comp/env/HikariDS")); + try { + jndi.getObjectInstance(ref, null, null, null); + fail(); + } + catch (RuntimeException e) { + assertTrue(e.getMessage().contains("JNDI context does not found")); + } + } + + private class BogusContext extends AbstractContext + { + @Override + public Context createSubcontext(Name name) throws NamingException + { + return null; + } + + @Override + public Object lookup(String name) throws NamingException + { + final HikariDataSource ds = new HikariDataSource(); + ds.setPoolName("TestJNDI"); + return ds; + } + } + + private class BogusContext2 extends AbstractContext + { + @Override + public Context createSubcontext(Name name) throws NamingException + { + return null; + } + + @Override + public Object lookup(String name) throws NamingException + { + return new StubDataSource(); + } + } + + private class BogusRef extends RefAddr + { + private static final long serialVersionUID = 1L; + + private String content; + BogusRef(String type, String content) + { + super(type); + this.content = content; + } + + @Override + public Object getContent() + { + return content; + } + } +} diff --git a/src/test/java/com/zaxxer/hikari/pool/TestMBean.java b/src/test/java/com/zaxxer/hikari/pool/TestMBean.java new file mode 100644 index 0000000..55ba733 --- /dev/null +++ b/src/test/java/com/zaxxer/hikari/pool/TestMBean.java @@ -0,0 +1,44 @@ +/* + * 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.pool.TestElf.newHikariConfig; + +import java.sql.SQLException; + +import org.junit.Test; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; + +public class TestMBean +{ + @Test + public void testMBeanRegistration() throws SQLException + { + HikariConfig config = newHikariConfig(); + config.setMinimumIdle(0); + config.setMaximumPoolSize(1); + config.setRegisterMbeans(true); + config.setConnectionTimeout(2800); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + try (HikariDataSource ds = new HikariDataSource(config)) { + // Close immediately + } + } +} diff --git a/src/test/java/com/zaxxer/hikari/pool/TestMetrics.java b/src/test/java/com/zaxxer/hikari/pool/TestMetrics.java new file mode 100644 index 0000000..fcba58e --- /dev/null +++ b/src/test/java/com/zaxxer/hikari/pool/TestMetrics.java @@ -0,0 +1,303 @@ +/* + * 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.pool.TestElf.newHikariConfig; +import static com.zaxxer.hikari.pool.TestElf.newHikariDataSource; +import static com.zaxxer.hikari.util.UtilityElf.quietlySleep; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.junit.Assume.assumeFalse; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.SortedMap; +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +import com.codahale.metrics.Histogram; +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.Result; +import com.codahale.metrics.health.HealthCheckRegistry; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import com.zaxxer.hikari.metrics.MetricsTrackerFactory; +import com.zaxxer.hikari.metrics.dropwizard.CodahaleMetricsTrackerFactory; +import com.zaxxer.hikari.util.UtilityElf; + +import shaded.org.codehaus.plexus.interpolation.os.Os; + +/** + * Test HikariCP/CodaHale metrics integration. + * + * @author Brett Wooldridge + */ +public class TestMetrics +{ + @Test + public void testMetricWait() throws SQLException + { + MetricRegistry metricRegistry = new MetricRegistry(); + + HikariConfig config = newHikariConfig(); + config.setMinimumIdle(1); + config.setMaximumPoolSize(1); + config.setMetricRegistry(metricRegistry); + config.setInitializationFailTimeout(Long.MAX_VALUE); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + try (HikariDataSource ds = new HikariDataSource(config)) { + ds.getConnection().close(); + + Timer timer = metricRegistry.getTimers(new MetricFilter() { + /** {@inheritDoc} */ + @Override + public boolean matches(String name, Metric metric) + { + return "testMetricWait.pool.Wait".equals(MetricRegistry.name("testMetricWait", "pool", "Wait")); + } + }).values().iterator().next(); + + assertEquals(1, timer.getCount()); + assertTrue(timer.getMeanRate() > 0.0); + } + } + + @Test + public void testMetricUsage() throws SQLException + { + assumeFalse(Os.isFamily(Os.FAMILY_WINDOWS)); + MetricRegistry metricRegistry = new MetricRegistry(); + + HikariConfig config = newHikariConfig(); + config.setMinimumIdle(1); + config.setMaximumPoolSize(1); + config.setMetricRegistry(metricRegistry); + config.setInitializationFailTimeout(0); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + try (HikariDataSource ds = new HikariDataSource(config)) { + try (Connection connection = ds.getConnection()) { + UtilityElf.quietlySleep(250L); + } + + Histogram histo = metricRegistry.getHistograms(new MetricFilter() { + /** {@inheritDoc} */ + @Override + public boolean matches(String name, Metric metric) + { + return name.equals(MetricRegistry.name("testMetricUsage", "pool", "Usage")); + } + }).values().iterator().next(); + + assertEquals(1, histo.getCount()); + double seventyFifth = histo.getSnapshot().get75thPercentile(); + assertTrue("Seventy-fith percentile less than 250ms: " + seventyFifth, seventyFifth >= 250.0); + } + } + + @Test + public void testHealthChecks() throws Exception + { + MetricRegistry metricRegistry = new MetricRegistry(); + HealthCheckRegistry healthRegistry = new HealthCheckRegistry(); + + HikariConfig config = newHikariConfig(); + config.setMaximumPoolSize(10); + config.setMetricRegistry(metricRegistry); + config.setHealthCheckRegistry(healthRegistry); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + config.addHealthCheckProperty("connectivityCheckTimeoutMs", "1000"); + config.addHealthCheckProperty("expected99thPercentileMs", "100"); + + try (HikariDataSource ds = new HikariDataSource(config)) { + quietlySleep(TimeUnit.SECONDS.toMillis(2)); + + try (Connection connection = ds.getConnection()) { + // close immediately + } + + try (Connection connection = ds.getConnection()) { + // close immediately + } + + SortedMap<String, Result> healthChecks = healthRegistry.runHealthChecks(); + + Result connectivityResult = healthChecks.get("testHealthChecks.pool.ConnectivityCheck"); + assertTrue(connectivityResult.isHealthy()); + + Result slaResult = healthChecks.get("testHealthChecks.pool.Connection99Percent"); + assertTrue(slaResult.isHealthy()); + } + } + + @Test + public void testSetters1() throws Exception + { + try (HikariDataSource ds = newHikariDataSource()) { + ds.setMaximumPoolSize(1); + ds.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + MetricRegistry metricRegistry = new MetricRegistry(); + HealthCheckRegistry healthRegistry = new HealthCheckRegistry(); + + try { + try (Connection connection = ds.getConnection()) { + // close immediately + } + + // After the pool as started, we can only set them once... + ds.setMetricRegistry(metricRegistry); + ds.setHealthCheckRegistry(healthRegistry); + + // and never again... + ds.setMetricRegistry(metricRegistry); + fail("Should not have been allowed to set registry after pool started"); + } + catch (IllegalStateException ise) { + // pass + try { + ds.setHealthCheckRegistry(healthRegistry); + fail("Should not have been allowed to set registry after pool started"); + } + catch (IllegalStateException ise2) { + // pass + } + } + } + } + + @Test + public void testSetters2() throws Exception + { + try (HikariDataSource ds = newHikariDataSource()) { + ds.setMaximumPoolSize(1); + ds.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + MetricRegistry metricRegistry = new MetricRegistry(); + HealthCheckRegistry healthRegistry = new HealthCheckRegistry(); + + ds.setMetricRegistry(metricRegistry); + ds.setHealthCheckRegistry(healthRegistry); + + // before the pool is started, we can set it any number of times... + ds.setMetricRegistry(metricRegistry); + ds.setHealthCheckRegistry(healthRegistry); + + try (Connection connection = ds.getConnection()) { + + // after the pool is started, we cannot set it any more + ds.setMetricRegistry(metricRegistry); + fail("Should not have been allowed to set registry after pool started"); + } + catch (IllegalStateException ise) { + // pass + } + } + } + + @Test + public void testSetters3() throws Exception + { + try (HikariDataSource ds = newHikariDataSource()) { + ds.setMaximumPoolSize(1); + ds.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + MetricRegistry metricRegistry = new MetricRegistry(); + MetricsTrackerFactory metricsTrackerFactory = new CodahaleMetricsTrackerFactory(metricRegistry); + + try (Connection connection = ds.getConnection()) { + + // After the pool as started, we can only set them once... + ds.setMetricsTrackerFactory(metricsTrackerFactory); + + // and never again... + ds.setMetricsTrackerFactory(metricsTrackerFactory); + fail("Should not have been allowed to set metricsTrackerFactory after pool started"); + } + catch (IllegalStateException ise) { + // pass + try { + // and never again... (even when calling another method) + ds.setMetricRegistry(metricRegistry); + fail("Should not have been allowed to set registry after pool started"); + } + catch (IllegalStateException ise2) { + // pass + } + } + } + } + + @Test + public void testSetters4() throws Exception + { + try (HikariDataSource ds = newHikariDataSource()) { + ds.setMaximumPoolSize(1); + ds.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + MetricRegistry metricRegistry = new MetricRegistry(); + + // before the pool is started, we can set it any number of times using either setter + ds.setMetricRegistry(metricRegistry); + ds.setMetricRegistry(metricRegistry); + ds.setMetricRegistry(metricRegistry); + + try (Connection connection = ds.getConnection()) { + + // after the pool is started, we cannot set it any more + ds.setMetricRegistry(metricRegistry); + fail("Should not have been allowed to set registry after pool started"); + } + catch (IllegalStateException ise) { + // pass + } + } + } + + @Test + public void testSetters5() throws Exception + { + try (HikariDataSource ds = newHikariDataSource()) { + ds.setMaximumPoolSize(1); + ds.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + MetricRegistry metricRegistry = new MetricRegistry(); + MetricsTrackerFactory metricsTrackerFactory = new CodahaleMetricsTrackerFactory(metricRegistry); + + // before the pool is started, we can set it any number of times using either setter + ds.setMetricsTrackerFactory(metricsTrackerFactory); + ds.setMetricsTrackerFactory(metricsTrackerFactory); + ds.setMetricsTrackerFactory(metricsTrackerFactory); + + try (Connection connection = ds.getConnection()) { + + // after the pool is started, we cannot set it any more + ds.setMetricsTrackerFactory(metricsTrackerFactory); + fail("Should not have been allowed to set registry factory after pool started"); + } + catch (IllegalStateException ise) { + // pass + } + } + } +} diff --git a/src/test/java/com/zaxxer/hikari/pool/TestPropertySetter.java b/src/test/java/com/zaxxer/hikari/pool/TestPropertySetter.java new file mode 100644 index 0000000..fb10446 --- /dev/null +++ b/src/test/java/com/zaxxer/hikari/pool/TestPropertySetter.java @@ -0,0 +1,111 @@ +/* + * 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.pool.TestElf.newHikariConfig; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.ByteArrayOutputStream; +import java.io.PrintWriter; +import java.util.Properties; +import java.util.Set; + +import javax.sql.DataSource; + +import org.junit.Test; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.util.PropertyElf; + +public class TestPropertySetter +{ + @Test + public void testProperty1() throws Exception + { + Properties propfile1 = new Properties(); + propfile1.load(TestPropertySetter.class.getResourceAsStream("/propfile1.properties")); + HikariConfig config = new HikariConfig(propfile1); + config.validate(); + + assertEquals(5, config.getMinimumIdle()); + assertEquals("SELECT 1", config.getConnectionTestQuery()); + } + + @Test + public void testProperty2() throws Exception + { + Properties propfile2 = new Properties(); + propfile2.load(TestPropertySetter.class.getResourceAsStream("/propfile2.properties")); + HikariConfig config = new HikariConfig(propfile2); + config.validate(); + + Class<?> clazz = this.getClass().getClassLoader().loadClass(config.getDataSourceClassName()); + DataSource dataSource = (DataSource) clazz.newInstance(); + PropertyElf.setTargetFromProperties(dataSource, config.getDataSourceProperties()); + } + + @Test + public void testObjectProperty() throws Exception + { + HikariConfig config = newHikariConfig(); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + PrintWriter writer = new PrintWriter(new ByteArrayOutputStream()); + config.addDataSourceProperty("logWriter", writer); + + Class<?> clazz = this.getClass().getClassLoader().loadClass(config.getDataSourceClassName()); + DataSource dataSource = (DataSource) clazz.newInstance(); + PropertyElf.setTargetFromProperties(dataSource, config.getDataSourceProperties()); + + assertSame(PrintWriter.class, dataSource.getLogWriter().getClass()); + } + + @Test + public void testPropertyUpperCase() throws Exception + { + Properties propfile3 = new Properties(); + propfile3.load(TestPropertySetter.class.getResourceAsStream("/propfile3.properties")); + HikariConfig config = new HikariConfig(propfile3); + config.validate(); + + Class<?> clazz = this.getClass().getClassLoader().loadClass(config.getDataSourceClassName()); + DataSource dataSource = (DataSource) clazz.newInstance(); + PropertyElf.setTargetFromProperties(dataSource, config.getDataSourceProperties()); + } + + @Test + public void testGetPropertyNames() throws Exception + { + Set<String> propertyNames = PropertyElf.getPropertyNames(HikariConfig.class); + assertTrue(propertyNames.contains("dataSourceClassName")); + } + + @Test + public void testSetNonExistantPropertyName() throws Exception + { + try { + Properties props = new Properties(); + props.put("what", "happened"); + PropertyElf.setTargetFromProperties(new HikariConfig(), props); + fail(); + } + catch (RuntimeException e) { + } + } +} diff --git a/src/test/java/com/zaxxer/hikari/pool/TestProxies.java b/src/test/java/com/zaxxer/hikari/pool/TestProxies.java new file mode 100644 index 0000000..96f4066 --- /dev/null +++ b/src/test/java/com/zaxxer/hikari/pool/TestProxies.java @@ -0,0 +1,324 @@ +/* + * 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.pool.TestElf.newHikariConfig; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import com.zaxxer.hikari.mocks.StubConnection; +import com.zaxxer.hikari.mocks.StubStatement; + +public class TestProxies +{ + @Test + public void testProxyCreation() throws SQLException + { + HikariConfig config = newHikariConfig(); + config.setMinimumIdle(0); + config.setMaximumPoolSize(1); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + try (HikariDataSource ds = new HikariDataSource(config)) { + Connection conn = ds.getConnection(); + + assertNotNull(conn.createStatement(ResultSet.FETCH_FORWARD, ResultSet.TYPE_SCROLL_INSENSITIVE)); + assertNotNull(conn.createStatement(ResultSet.FETCH_FORWARD, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.HOLD_CURSORS_OVER_COMMIT)); + assertNotNull(conn.prepareCall("some sql")); + assertNotNull(conn.prepareCall("some sql", ResultSet.FETCH_FORWARD, ResultSet.TYPE_SCROLL_INSENSITIVE)); + assertNotNull(conn.prepareCall("some sql", ResultSet.FETCH_FORWARD, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.HOLD_CURSORS_OVER_COMMIT)); + assertNotNull(conn.prepareStatement("some sql", PreparedStatement.NO_GENERATED_KEYS)); + assertNotNull(conn.prepareStatement("some sql", new int[3])); + assertNotNull(conn.prepareStatement("some sql", new String[3])); + assertNotNull(conn.prepareStatement("some sql", ResultSet.FETCH_FORWARD, ResultSet.TYPE_SCROLL_INSENSITIVE)); + assertNotNull(conn.prepareStatement("some sql", ResultSet.FETCH_FORWARD, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.HOLD_CURSORS_OVER_COMMIT)); + assertNotNull(conn.toString()); + + assertTrue(conn.isWrapperFor(Connection.class)); + assertTrue(conn.isValid(10)); + assertFalse(conn.isClosed()); + assertTrue(conn.unwrap(StubConnection.class) instanceof StubConnection); + try { + conn.unwrap(TestProxies.class); + fail(); + } + catch (SQLException e) { + // pass + } + } + } + + @Test + public void testStatementProxy() throws SQLException + { + HikariConfig config = newHikariConfig(); + config.setMinimumIdle(0); + config.setMaximumPoolSize(1); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + try (HikariDataSource ds = new HikariDataSource(config)) { + Connection conn = ds.getConnection(); + + PreparedStatement stmt = conn.prepareStatement("some sql"); + stmt.executeQuery(); + stmt.executeQuery("some sql"); + assertFalse(stmt.isClosed()); + assertNotNull(stmt.getGeneratedKeys()); + assertNotNull(stmt.getResultSet()); + assertNotNull(stmt.getConnection()); + assertTrue(stmt.unwrap(StubStatement.class) instanceof StubStatement); + try { + stmt.unwrap(TestProxies.class); + fail(); + } + catch (SQLException e) { + // pass + } + } + } + + @Test + public void testStatementExceptions() throws SQLException + { + HikariConfig config = newHikariConfig(); + config.setMinimumIdle(0); + config.setMaximumPoolSize(1); + config.setConnectionTimeout(TimeUnit.SECONDS.toMillis(1)); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + try (HikariDataSource ds = new HikariDataSource(config)) { + Connection conn = ds.getConnection(); + StubConnection stubConnection = conn.unwrap(StubConnection.class); + stubConnection.throwException = true; + + try { + conn.createStatement(); + fail(); + } + catch (SQLException e) { + // pass + } + + try { + conn.createStatement(0, 0); + fail(); + } + catch (SQLException e) { + // pass + } + + try { + conn.createStatement(0, 0, 0); + fail(); + } + catch (SQLException e) { + // pass + } + + try { + conn.prepareCall(""); + fail(); + } + catch (SQLException e) { + // pass + } + + try { + conn.prepareCall("", 0, 0); + fail(); + } + catch (SQLException e) { + // pass + } + + try { + conn.prepareCall("", 0, 0, 0); + fail(); + } + catch (SQLException e) { + // pass + } + + try { + conn.prepareStatement(""); + fail(); + } + catch (SQLException e) { + // pass + } + + try { + conn.prepareStatement("", 0); + fail(); + } + catch (SQLException e) { + // pass + } + + try { + conn.prepareStatement("", new int[0]); + fail(); + } + catch (SQLException e) { + // pass + } + + try { + conn.prepareStatement("", new String[0]); + fail(); + } + catch (SQLException e) { + // pass + } + + try { + conn.prepareStatement("", 0, 0); + fail(); + } + catch (SQLException e) { + // pass + } + + try { + conn.prepareStatement("", 0, 0, 0); + fail(); + } + catch (SQLException e) { + // pass + } + } + } + + @Test + public void testOtherExceptions() throws SQLException + { + HikariConfig config = newHikariConfig(); + config.setMinimumIdle(0); + config.setMaximumPoolSize(1); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + try (HikariDataSource ds = new HikariDataSource(config)) { + try (Connection conn = ds.getConnection()) { + StubConnection stubConnection = conn.unwrap(StubConnection.class); + stubConnection.throwException = true; + + try { + conn.setTransactionIsolation(Connection.TRANSACTION_NONE); + fail(); + } + catch (SQLException e) { + // pass + } + + try { + conn.isReadOnly(); + fail(); + } + catch (SQLException e) { + // pass + } + + try { + conn.setReadOnly(false); + fail(); + } + catch (SQLException e) { + // pass + } + + try { + conn.setCatalog(""); + fail(); + } + catch (SQLException e) { + // pass + } + + try { + conn.setAutoCommit(false); + fail(); + } + catch (SQLException e) { + // pass + } + + try { + conn.clearWarnings(); + fail(); + } + catch (SQLException e) { + // pass + } + + try { + conn.isValid(0); + fail(); + } + catch (SQLException e) { + // pass + } + + try { + conn.isWrapperFor(getClass()); + fail(); + } + catch (SQLException e) { + // pass + } + + try { + conn.unwrap(getClass()); + fail(); + } + catch (SQLException e) { + // pass + } + + try { + conn.close(); + fail(); + } + catch (SQLException e) { + // pass + } + + try { + assertFalse(conn.isValid(0)); + } + catch (SQLException e) { + fail(); + } + } + } + } +} diff --git a/src/test/java/com/zaxxer/hikari/pool/TestValidation.java b/src/test/java/com/zaxxer/hikari/pool/TestValidation.java new file mode 100644 index 0000000..46ba779 --- /dev/null +++ b/src/test/java/com/zaxxer/hikari/pool/TestValidation.java @@ -0,0 +1,253 @@ +/* + * 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.pool.TestElf.newHikariConfig; +import static com.zaxxer.hikari.pool.TestElf.setSlf4jTargetStream; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +import com.zaxxer.hikari.HikariConfig; + +/** + * @author Brett Wooldridge + */ +public class TestValidation +{ + @Test + public void validateLoadProperties() + { + System.setProperty("hikaricp.configurationFile", "/propfile1.properties"); + HikariConfig config = newHikariConfig(); + System.clearProperty("hikaricp.configurationFile"); + assertEquals(5, config.getMinimumIdle()); + } + + @Test + public void validateMissingProperties() + { + try { + HikariConfig config = new HikariConfig("missing"); + config.validate(); + } + catch (IllegalArgumentException ise) { + assertTrue(ise.getMessage().contains("property file")); + } + } + + @Test + public void validateMissingDS() + { + try { + HikariConfig config = newHikariConfig(); + config.validate(); + fail(); + } + catch (IllegalArgumentException ise) { + assertTrue(ise.getMessage().contains("dataSource or dataSourceClassName or jdbcUrl is required.")); + } + } + + @Test + public void validateMissingUrl() + { + try { + HikariConfig config = newHikariConfig(); + config.setDriverClassName("com.zaxxer.hikari.mocks.StubDriver"); + config.validate(); + fail(); + } + catch (IllegalArgumentException ise) { + assertTrue(ise.getMessage().contains("jdbcUrl is required with driverClassName")); + } + } + + @Test + public void validateDriverAndUrl() + { + try { + HikariConfig config = newHikariConfig(); + config.setDriverClassName("com.zaxxer.hikari.mocks.StubDriver"); + config.setJdbcUrl("jdbc:stub"); + config.validate(); + } + catch (Throwable t) { + fail(t.getMessage()); + } + } + + @Test + public void validateBadDriver() + { + try { + HikariConfig config = newHikariConfig(); + config.setDriverClassName("invalid"); + config.validate(); + fail(); + } + catch (RuntimeException ise) { + assertTrue(ise.getMessage().contains("class of driverClassName ")); + } + } + + @Test + public void validateInvalidConnectionTimeout() + { + try { + HikariConfig config = newHikariConfig(); + config.setConnectionTimeout(10L); + fail(); + } + catch (IllegalArgumentException ise) { + assertTrue(ise.getMessage().contains("connectionTimeout cannot be less than 250ms")); + } + } + + @Test + public void validateInvalidValidationTimeout() + { + try { + HikariConfig config = newHikariConfig(); + config.setValidationTimeout(10L); + fail(); + } + catch (IllegalArgumentException ise) { + assertTrue(ise.getMessage().contains("validationTimeout cannot be less than 250ms")); + } + } + + @Test + public void validateInvalidIdleTimeout() + { + try { + HikariConfig config = newHikariConfig(); + config.setIdleTimeout(-1L); + fail("negative idle timeout accepted"); + } + catch (IllegalArgumentException ise) { + assertTrue(ise.getMessage().contains("idleTimeout cannot be negative")); + } + } + + @Test + public void validateIdleTimeoutTooSmall() + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + PrintStream ps = new PrintStream(baos, true); + setSlf4jTargetStream(HikariConfig.class, ps); + + HikariConfig config = newHikariConfig(); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + config.setIdleTimeout(TimeUnit.SECONDS.toMillis(5)); + config.validate(); + assertTrue(new String(baos.toByteArray()).contains("less than 10000ms")); + } + + @Test + public void validateIdleTimeoutExceedsLifetime() + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + PrintStream ps = new PrintStream(baos, true); + setSlf4jTargetStream(HikariConfig.class, ps); + + HikariConfig config = newHikariConfig(); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + config.setMaxLifetime(TimeUnit.MINUTES.toMillis(2)); + config.setIdleTimeout(TimeUnit.MINUTES.toMillis(3)); + config.validate(); + + String s = new String(baos.toByteArray()); + assertTrue("idleTimeout is close to or more than maxLifetime, disabling it." + s + "*", s.contains("is close to or more than maxLifetime")); + } + + @Test + public void validateInvalidMinIdle() + { + try { + HikariConfig config = newHikariConfig(); + config.setMinimumIdle(-1); + fail(); + } + catch (IllegalArgumentException ise) { + assertTrue(ise.getMessage().contains("minimumIdle cannot be negative")); + } + } + + @Test + public void validateInvalidMaxPoolSize() + { + try { + HikariConfig config = newHikariConfig(); + config.setMaximumPoolSize(0); + fail(); + } + catch (IllegalArgumentException ise) { + assertTrue(ise.getMessage().contains("maxPoolSize cannot be less than 1")); + } + } + + @Test + public void validateInvalidLifetime() + { + try { + HikariConfig config = newHikariConfig(); + config.setConnectionTimeout(Integer.MAX_VALUE); + config.setIdleTimeout(1000L); + config.setMaxLifetime(-1L); + config.setLeakDetectionThreshold(1000L); + config.validate(); + fail(); + } + catch (IllegalArgumentException ise) { + // pass + } + } + + @Test + public void validateInvalidLeakDetection() + { + try { + HikariConfig config = newHikariConfig(); + config.setLeakDetectionThreshold(1000L); + config.validate(); + fail(); + } + catch (IllegalArgumentException ise) { + // pass + } + } + + @Test + public void validateZeroConnectionTimeout() + { + try { + HikariConfig config = newHikariConfig(); + config.setConnectionTimeout(0); + config.validate(); + assertEquals(Integer.MAX_VALUE, config.getConnectionTimeout()); + } + catch (IllegalArgumentException ise) { + // pass + } + } +} diff --git a/src/test/java/com/zaxxer/hikari/pool/UnwrapTest.java b/src/test/java/com/zaxxer/hikari/pool/UnwrapTest.java new file mode 100644 index 0000000..d13c8f4 --- /dev/null +++ b/src/test/java/com/zaxxer/hikari/pool/UnwrapTest.java @@ -0,0 +1,90 @@ +/* + * 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 static com.zaxxer.hikari.pool.TestElf.newHikariConfig; +import static com.zaxxer.hikari.pool.TestElf.getPool; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import java.sql.Connection; +import java.sql.SQLException; + +import org.junit.Test; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import com.zaxxer.hikari.mocks.StubConnection; +import com.zaxxer.hikari.mocks.StubDataSource; + +/** + * @author Brett Wooldridge + */ +public class UnwrapTest +{ + @Test + public void testUnwrapConnection() throws SQLException + { + HikariConfig config = newHikariConfig(); + config.setMinimumIdle(1); + config.setMaximumPoolSize(1); + config.setInitializationFailTimeout(0); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + try (HikariDataSource ds = new HikariDataSource(config)) { + ds.getConnection().close(); + assertSame("Idle connections not as expected", 1, getPool(ds).getIdleConnections()); + + Connection connection = ds.getConnection(); + assertNotNull(connection); + + StubConnection unwrapped = connection.unwrap(StubConnection.class); + assertTrue("unwrapped connection is not instance of StubConnection: " + unwrapped, (unwrapped != null && unwrapped instanceof StubConnection)); + } + } + + @Test + public void testUnwrapDataSource() throws SQLException + { + HikariConfig config = newHikariConfig(); + config.setMinimumIdle(1); + config.setMaximumPoolSize(1); + config.setInitializationFailTimeout(0); + config.setConnectionTestQuery("VALUES 1"); + config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource"); + + try (HikariDataSource ds = new HikariDataSource(config)) { + StubDataSource unwrap = ds.unwrap(StubDataSource.class); + assertNotNull(unwrap); + assertTrue(unwrap instanceof StubDataSource); + + assertTrue(ds.isWrapperFor(HikariDataSource.class)); + assertTrue(ds.unwrap(HikariDataSource.class) instanceof HikariDataSource); + + assertFalse(ds.isWrapperFor(getClass())); + try { + ds.unwrap(getClass()); + } + catch (SQLException e) { + assertTrue(e.getMessage().contains("Wrapped DataSource")); + } + } + } +} diff --git a/src/test/java/com/zaxxer/hikari/util/ClockSourceTest.java b/src/test/java/com/zaxxer/hikari/util/ClockSourceTest.java new file mode 100644 index 0000000..2d0482a --- /dev/null +++ b/src/test/java/com/zaxxer/hikari/util/ClockSourceTest.java @@ -0,0 +1,60 @@ +/* + * 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.util; + +import org.junit.Assert; +import org.junit.Test; + +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; + +/** + * + * @author Brett Wooldridge + */ +public class ClockSourceTest +{ + @Test + public void testClockSourceDisplay() + { + ClockSource msSource = new ClockSource.MillisecondClockSource(); + + final long sTime = DAYS.toMillis(3) + HOURS.toMillis(9) + MINUTES.toMillis(24) + SECONDS.toMillis(18) + MILLISECONDS.toMillis(572); + + final long eTime = DAYS.toMillis(4) + HOURS.toMillis(9) + MINUTES.toMillis(55) + SECONDS.toMillis(23) + MILLISECONDS.toMillis(777); + String ds1 = msSource.elapsedDisplayString0(sTime, eTime); + Assert.assertEquals("1d31m5s205ms", ds1); + + final long eTime2 = DAYS.toMillis(3) + HOURS.toMillis(8) + MINUTES.toMillis(24) + SECONDS.toMillis(23) + MILLISECONDS.toMillis(777); + String ds2 = msSource.elapsedDisplayString0(sTime, eTime2); + Assert.assertEquals("-59m54s795ms", ds2); + + + ClockSource nsSource = new ClockSource.NanosecondClockSource(); + + final long sTime2 = DAYS.toNanos(3) + HOURS.toNanos(9) + MINUTES.toNanos(24) + SECONDS.toNanos(18) + MILLISECONDS.toNanos(572) + MICROSECONDS.toNanos(324) + NANOSECONDS.toNanos(823); + + final long eTime3 = DAYS.toNanos(4) + HOURS.toNanos(19) + MINUTES.toNanos(55) + SECONDS.toNanos(23) + MILLISECONDS.toNanos(777) + MICROSECONDS.toNanos(0) + NANOSECONDS.toNanos(982); + String ds3 = nsSource.elapsedDisplayString0(sTime2, eTime3); + Assert.assertEquals("1d10h31m5s204ms676µs159ns", ds3); + } +} diff --git a/src/test/java/com/zaxxer/hikari/util/TestFastList.java b/src/test/java/com/zaxxer/hikari/util/TestFastList.java new file mode 100644 index 0000000..089ff5a --- /dev/null +++ b/src/test/java/com/zaxxer/hikari/util/TestFastList.java @@ -0,0 +1,172 @@ +/* + * 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 org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; + +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Iterator; + +import org.junit.Test; + +import com.zaxxer.hikari.mocks.StubStatement; + +public class TestFastList +{ + @Test + public void testAddRemove() + { + ArrayList<Statement> verifyList = new ArrayList<>(); + + FastList<Statement> list = new FastList<>(Statement.class); + for (int i = 0; i < 32; i++) + { + StubStatement statement = new StubStatement(null); + list.add(statement); + verifyList.add(statement); + } + + for (int i = 0; i < 32; i++) + { + assertNotNull("Element " + i + " was null but should be " + verifyList.get(i), list.get(0)); + int size = list.size(); + list.remove(verifyList.get(i)); + assertSame(size - 1, list.size()); + } + } + + @Test + public void testAddRemoveTail() + { + ArrayList<Statement> verifyList = new ArrayList<>(); + + FastList<Statement> list = new FastList<>(Statement.class); + for (int i = 0; i < 32; i++) + { + StubStatement statement = new StubStatement(null); + list.add(statement); + verifyList.add(statement); + } + + for (int i = 31; i >= 0; i--) + { + assertNotNull("Element " + i, list.get(i)); + int size = list.size(); + list.remove(verifyList.get(i)); + assertSame(size - 1, list.size()); + } + } + + @Test + public void testOverflow() + { + ArrayList<Statement> verifyList = new ArrayList<>(); + + FastList<Statement> list = new FastList<>(Statement.class); + for (int i = 0; i < 100; i++) + { + StubStatement statement = new StubStatement(null); + list.add(statement); + verifyList.add(statement); + } + + for (int i = 0; i < 100; i++) + { + assertNotNull("Element " + i, list.get(i)); + assertSame(verifyList.get(i), list.get(i)); + } + } + + @Test + public void testIterator() + { + FastList<Statement> list = new FastList<>(Statement.class); + for (int i = 0; i < 100; i++) + { + StubStatement statement = new StubStatement(null); + list.add(statement); + } + + Iterator<Statement> iter = list.iterator(); + for (int i = 0; i < list.size(); i++) { + assertSame(list.get(i), iter.next()); + } + } + + @Test + public void testClear() + { + FastList<Statement> list = new FastList<>(Statement.class); + for (int i = 0; i < 100; i++) + { + StubStatement statement = new StubStatement(null); + list.add(statement); + } + + assertNotEquals(0, list.size()); + list.clear(); + assertEquals(0, list.size()); + } + + @Test + public void testRemoveLast() + { + FastList<Statement> list = new FastList<>(Statement.class); + + Statement last = null; + for (int i = 0; i < 100; i++) + { + StubStatement statement = new StubStatement(null); + list.add(statement); + last = statement; + } + + assertEquals(last, list.removeLast()); + assertEquals(99, list.size()); + } + + @Test + public void testPolyMorphism1() + { + class Foo implements Base2 { + + } + + class Bar extends Foo { + + } + + FastList<Base> list = new FastList<>(Base.class, 2); + list.add(new Foo()); + list.add(new Foo()); + list.add(new Bar()); + } + + interface Base + { + + } + + interface Base2 extends Base + { + + } +} diff --git a/src/test/java/com/zaxxer/hikari/util/TomcatConcurrentBagLeakTest.java b/src/test/java/com/zaxxer/hikari/util/TomcatConcurrentBagLeakTest.java new file mode 100644 index 0000000..06957b2 --- /dev/null +++ b/src/test/java/com/zaxxer/hikari/util/TomcatConcurrentBagLeakTest.java @@ -0,0 +1,328 @@ +/* + * 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.util; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import java.io.DataInputStream; +import java.io.IOException; +import java.lang.ref.Reference; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.net.URL; +import java.util.concurrent.CompletableFuture; + +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.runners.MethodSorters; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.zaxxer.hikari.util.ConcurrentBag.IConcurrentBagEntry; + +/** + * @author Brett Wooldridge + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class TomcatConcurrentBagLeakTest +{ + @Test + public void testConcurrentBagForLeaks() throws Exception + { + ClassLoader cl = new FauxWebClassLoader(); + Class<?> clazz = cl.loadClass(this.getClass().getName() + "$FauxWebContext"); + Object fauxWebContext = clazz.newInstance(); + + Method createConcurrentBag = clazz.getDeclaredMethod("createConcurrentBag"); + createConcurrentBag.invoke(fauxWebContext); + + Field failureException = clazz.getDeclaredField("failureException"); + Exception ex = (Exception) failureException.get(fauxWebContext); + assertNull(ex); + } + + @Test + public void testConcurrentBagForLeaks2() throws Exception + { + ClassLoader cl = this.getClass().getClassLoader(); + Class<?> clazz = cl.loadClass(this.getClass().getName() + "$FauxWebContext"); + Object fauxWebContext = clazz.newInstance(); + + Method createConcurrentBag = clazz.getDeclaredMethod("createConcurrentBag"); + createConcurrentBag.invoke(fauxWebContext); + + Field failureException = clazz.getDeclaredField("failureException"); + Exception ex = (Exception) failureException.get(fauxWebContext); + assertNotNull(ex); + } + + static class FauxWebClassLoader extends ClassLoader + { + static final byte[] classBytes = new byte[16_000]; + + @Override + public Class<?> loadClass(String name) throws ClassNotFoundException + { + if (name.startsWith("java") || name.startsWith("org")) { + return super.loadClass(name, true); + } + + final String resourceName = "/" + name.replace('.', '/') + ".class"; + final URL resource = this.getClass().getResource(resourceName); + try (DataInputStream is = new DataInputStream(resource.openStream())) { + int read = 0; + while (read < classBytes.length) { + final int rc = is.read(classBytes, read, classBytes.length - read); + if (rc == -1) { + break; + } + read += rc; + } + + return defineClass(name, classBytes, 0, read); + } + catch (IOException e) { + throw new ClassNotFoundException(name); + } + } + } + + public static class PoolEntry implements IConcurrentBagEntry + { + private int state; + + @Override + public boolean compareAndSet(int expectState, int newState) + { + this.state = newState; + return true; + } + + @Override + public void setState(int newState) + { + this.state = newState; + } + + @Override + public int getState() + { + return state; + } + } + + public static class FauxWebContext + { + private static final Logger log = LoggerFactory.getLogger(FauxWebContext.class); + + public Exception failureException; + + public void createConcurrentBag() throws InterruptedException + { + try (ConcurrentBag<PoolEntry> bag = new ConcurrentBag<>((x) -> CompletableFuture.completedFuture(Boolean.TRUE))) { + + PoolEntry entry = new PoolEntry(); + bag.add(entry); + + PoolEntry borrowed = bag.borrow(100, MILLISECONDS); + bag.requite(borrowed); + + PoolEntry removed = bag.borrow(100, MILLISECONDS); + bag.remove(removed); + } + + checkThreadLocalsForLeaks(); + } + + private void checkThreadLocalsForLeaks() + { + Thread[] threads = getThreads(); + + try { + // Make the fields in the Thread class that store ThreadLocals + // accessible + Field threadLocalsField = Thread.class.getDeclaredField("threadLocals"); + threadLocalsField.setAccessible(true); + Field inheritableThreadLocalsField = Thread.class.getDeclaredField("inheritableThreadLocals"); + inheritableThreadLocalsField.setAccessible(true); + // Make the underlying array of ThreadLoad.ThreadLocalMap.Entry objects + // accessible + Class<?> tlmClass = Class.forName("java.lang.ThreadLocal$ThreadLocalMap"); + Field tableField = tlmClass.getDeclaredField("table"); + tableField.setAccessible(true); + Method expungeStaleEntriesMethod = tlmClass.getDeclaredMethod("expungeStaleEntries"); + expungeStaleEntriesMethod.setAccessible(true); + + for (int i = 0; i < threads.length; i++) { + Object threadLocalMap; + if (threads[i] != null) { + + // Clear the first map + threadLocalMap = threadLocalsField.get(threads[i]); + if (null != threadLocalMap) { + expungeStaleEntriesMethod.invoke(threadLocalMap); + checkThreadLocalMapForLeaks(threadLocalMap, tableField); + } + + // Clear the second map + threadLocalMap = inheritableThreadLocalsField.get(threads[i]); + if (null != threadLocalMap) { + expungeStaleEntriesMethod.invoke(threadLocalMap); + checkThreadLocalMapForLeaks(threadLocalMap, tableField); + } + } + } + } + catch (Throwable t) { + log.warn("Failed to check for ThreadLocal references for web application [{}]", t); + failureException = new Exception(); + } + } + + private Object getContextName() + { + return this.getClass().getName(); + } + + // THE FOLLOWING CODE COPIED FROM APACHE TOMCAT (2017/01/08) + + /** + * Analyzes the given thread local map object. Also pass in the field that + * points to the internal table to save re-calculating it on every + * call to this method. + */ + private void checkThreadLocalMapForLeaks(Object map, Field internalTableField) throws IllegalAccessException, NoSuchFieldException + { + if (map != null) { + Object[] table = (Object[]) internalTableField.get(map); + if (table != null) { + for (int j = 0; j < table.length; j++) { + Object obj = table[j]; + if (obj != null) { + boolean keyLoadedByWebapp = false; + boolean valueLoadedByWebapp = false; + // Check the key + Object key = ((Reference<?>) obj).get(); + if (this.equals(key) || loadedByThisOrChild(key)) { + keyLoadedByWebapp = true; + } + // Check the value + Field valueField = obj.getClass().getDeclaredField("value"); + valueField.setAccessible(true); + Object value = valueField.get(obj); + if (this.equals(value) || loadedByThisOrChild(value)) { + valueLoadedByWebapp = true; + } + if (keyLoadedByWebapp || valueLoadedByWebapp) { + Object[] args = new Object[5]; + args[0] = getContextName(); + if (key != null) { + args[1] = getPrettyClassName(key.getClass()); + try { + args[2] = key.toString(); + } + catch (Exception e) { + log.warn("Unable to determine string representation of key of type [{}]", args[1], e); + args[2] = "Unknown"; + } + } + if (value != null) { + args[3] = getPrettyClassName(value.getClass()); + try { + args[4] = value.toString(); + } + catch (Exception e) { + log.warn("webappClassLoader.checkThreadLocalsForLeaks.badValue {}", args[3], e); + args[4] = "Unknown"; + } + } + + if (valueLoadedByWebapp) { + log.error("The web application [{}] created a ThreadLocal with key " + + "(value [{}]) and a value of type [{}] (value [{}]) but failed to remove " + + "it when the web application was stopped. Threads are going to be renewed " + + "over time to try and avoid a probable memory leak.", args); + failureException = new Exception(); + } + else if (value == null) { + log.debug("The web application [{}] created a ThreadLocal with key of type [{}] " + + "(value [{}]). The ThreadLocal has been correctly set to null and the " + + "key will be removed by GC.", args); + failureException = new Exception(); + } + else { + log.debug("The web application [{}] created a ThreadLocal with key of type [{}] " + + "(value [{}]) and a value of type [{}] (value [{}]). Since keys are only " + + "weakly held by the ThreadLocal Map this is not a memory leak.", args); + failureException = new Exception(); + } + } + } + } + } + } + } + + private boolean loadedByThisOrChild(Object key) + { + return key.getClass().getClassLoader() == this.getClass().getClassLoader(); + } + + /* + * Get the set of current threads as an array. + */ + private Thread[] getThreads() + { + // Get the current thread group + ThreadGroup tg = Thread.currentThread().getThreadGroup(); + // Find the root thread group + try { + while (tg.getParent() != null) { + tg = tg.getParent(); + } + } + catch (SecurityException se) { + log.warn("Unable to obtain the parent for ThreadGroup [{}]. It will not be possible to check all threads for potential memory leaks", tg.getName(), se); + } + + int threadCountGuess = tg.activeCount() + 50; + Thread[] threads = new Thread[threadCountGuess]; + int threadCountActual = tg.enumerate(threads); + // Make sure we don't miss any threads + while (threadCountActual == threadCountGuess) { + threadCountGuess *= 2; + threads = new Thread[threadCountGuess]; + // Note tg.enumerate(Thread[]) silently ignores any threads that + // can't fit into the array + threadCountActual = tg.enumerate(threads); + } + + return threads; + } + + private String getPrettyClassName(Class<?> clazz) + { + String name = clazz.getCanonicalName(); + if (name == null) { + name = clazz.getName(); + } + return name; + } + } +} diff --git a/src/test/resources/hibernate.properties b/src/test/resources/hibernate.properties new file mode 100644 index 0000000..6d7ebd9 --- /dev/null +++ b/src/test/resources/hibernate.properties @@ -0,0 +1,4 @@ +hibernate.hikari.minimumIdle=5 +hibernate.hikari.connectionTestQuery=SELECT 1 +hibernate.hikari.dataSourceClassName=com.zaxxer.hikari.mocks.StubDataSource +hibernate.connection.autocommit=false diff --git a/src/test/resources/log4j2-test.xml b/src/test/resources/log4j2-test.xml new file mode 100644 index 0000000..c316dfa --- /dev/null +++ b/src/test/resources/log4j2-test.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<configuration status="OFF"> + <appenders> + <Console name="Console" target="SYSTEM_ERR"> + <PatternLayout pattern="%6r [seq%5sn] [%-26.26t] %-5level %-20c{1} - %msg%n" /> + </Console> + </appenders> + <Loggers> + <Root level="info"> + <AppenderRef ref="Console" /> + </Root> + </Loggers> +</configuration> diff --git a/src/test/resources/propfile1.properties b/src/test/resources/propfile1.properties new file mode 100644 index 0000000..bf00bdd --- /dev/null +++ b/src/test/resources/propfile1.properties @@ -0,0 +1,4 @@ +minimumIdle=5 +connectionTestQuery=SELECT 1 +autoCommit=false +dataSourceClassName=com.zaxxer.hikari.mocks.StubDataSource diff --git a/src/test/resources/propfile2.properties b/src/test/resources/propfile2.properties new file mode 100644 index 0000000..94c7aa4 --- /dev/null +++ b/src/test/resources/propfile2.properties @@ -0,0 +1,5 @@ +connectionTestQuery=SELECT 1 +autoCommit=false +dataSourceClassName=com.zaxxer.hikari.mocks.StubDataSource +dataSource.user=test +dataSource.password=test
\ No newline at end of file diff --git a/src/test/resources/propfile3.properties b/src/test/resources/propfile3.properties new file mode 100644 index 0000000..10fdcf1 --- /dev/null +++ b/src/test/resources/propfile3.properties @@ -0,0 +1,6 @@ +connectionTestQuery=SELECT 1 +autoCommit=false +dataSourceClassName=com.zaxxer.hikari.mocks.StubDataSource +dataSource.user=test +dataSource.password=test +dataSource.url=jdbc:stub:foo |