diff --git a/quickfixj-all/pom.xml b/quickfixj-all/pom.xml index ac9843cc7b..a1364ac392 100644 --- a/quickfixj-all/pom.xml +++ b/quickfixj-all/pom.xml @@ -83,7 +83,7 @@ com.sleepycat*;resolution:=optional, org.apache.maven*;resolution:=optional, org.codehaus.plexus*;resolution:=optional, - org.logicalcobwebs.proxool*;resolution:=optional, + com.zaxxer*;resolution:=optional, org.dom4j*;resolution:=optional, * diff --git a/quickfixj-core/pom.xml b/quickfixj-core/pom.xml index cd7c61e943..012e816158 100644 --- a/quickfixj-core/pom.xml +++ b/quickfixj-core/pom.xml @@ -66,50 +66,10 @@ slf4j-api ${slf4j.version} - - - com.cloudhopper.proxool - proxool - 0.9.1 - true - - - - avalon-framework - avalon-framework-api - - - - commons-logging - commons-logging - - - - - com.cloudhopper.proxool - proxool-cglib - 0.9.1 - true - - - - avalon-framework - avalon-framework-api - - - - commons-logging - commons-logging - - - - - org.slf4j - jcl-over-slf4j - ${slf4j.version} - runtime - true + com.zaxxer + HikariCP + 4.0.3 com.sleepycat diff --git a/quickfixj-core/src/main/doc/usermanual/installation.html b/quickfixj-core/src/main/doc/usermanual/installation.html index f05bea37a7..a34f957737 100644 --- a/quickfixj-core/src/main/doc/usermanual/installation.html +++ b/quickfixj-core/src/main/doc/usermanual/installation.html @@ -81,16 +81,10 @@

Optional run-time libraries:

use Log4J logging. - proxool.jar + HikariCP.jar This JAR provided database connection pooling capabilities. It is required if you are using the JDBC store or log. - - jcl104-over-slf4j.jar - Adapts Jakarta Commons Logging to SLF4J. Required if you are using an optional - library that depends on Jakarta Commons Logging. Currently, this includes Proxool - (needed by JDBC store and log for connection pooling). - sleepycat-je.jar Needed if the SleepyCat JE message store is used. diff --git a/quickfixj-core/src/main/doc/usermanual/usage/configuration.html b/quickfixj-core/src/main/doc/usermanual/usage/configuration.html index 4b39200dfa..679c1bec1c 100644 --- a/quickfixj-core/src/main/doc/usermanual/usage/configuration.html +++ b/quickfixj-core/src/main/doc/usermanual/usage/configuration.html @@ -970,7 +970,7 @@

QuickFIX Settings

JdbcDriver JDBC driver for JDBC logger. Also used for JDBC log. Class name for the JDBC driver. Specify driver properties directly will cause the - creation of a Proxool data source that supports connection pooling. If you are using a + creation of a HikarCP data source that supports connection pooling. If you are using a database with it's own pooling data source (e.g., Oracle) then use the setDataSource() method on the Jdbc-related factories to set the data source directly.   @@ -1031,39 +1031,69 @@

QuickFIX Settings

Any nonempty string. "" (empty string) - JdbcMaxActiveConnection Specifies the maximum number of connections to the database. - Any number + Positive number 32 - JdbcMaxActiveTime - Specifies if the housekeeper comes across a thread that has been active for longer than - this (milliseconds) then it will kill it. So make sure you set this to a number bigger than your - slowest expected response! - Any number - 5000 + JdbcMinIdleConnection + 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 the best effort to restore them quickly and + efficiently. + + [0, JdbcMaxActiveConnection] + Same as JdbcMaxActiveConnection JdbcMaxConnectionLifeTime Specifies the maximum amount of time that a connection exists for before it is killed (milliseconds). - Any number - 28800000 - - - JdbcSimultaneousBuildThrottle - Specifies the maximum number of connections we can be building at any one time. - That is, the number of new connections that have been requested but aren't yet - available for use. Because connections can be built using more than one thread - (for instance, when they are built on demand) and it takes a finite time between - deciding to build the connection and it becoming available we need some way of - ensuring that a lot of threads don't all decide to build a connection at once. - (We could solve this in a smarter way - and indeed we will one day) - Any number - 32 + Positive + 28800000 ms (8 hours) + + + JdbcConnectionTimeout + 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 javax.sql.DataSource.getConnection(). + Non-negative number + 250 ms + + + JdbcConnectionIdleTimeout + Controls the maximum amount of time 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. + Non-negative number + 600000 ms (10 minutes) + + + JdbcConnectionKeepaliveTime + Controls the keepalive interval for a connection in the pool. An in-use connection will never be + tested by the keepalive thread, only when it is idle will it be tested. + Non-negative number + 0 ms + + + JdbcConnectionKeepaliveTime + Controls the keepalive interval for a connection in the pool. An in-use connection will never be + tested by the keepalive thread, only when it is idle will it be tested. + Non-negative number + 0 ms + + + JdbcConnectionTestQuery + Set the SQL query to be executed to test the validity of connections. Using the JDBC4 + Connection.isValid() method to test connection validity can be more efficient on some + databases and is recommended. If your driver supports JDBC4 we strongly recommend not + setting this property. + Valid SQL query + + diff --git a/quickfixj-core/src/main/java/quickfix/JdbcLog.java b/quickfixj-core/src/main/java/quickfix/JdbcLog.java index 308e37fade..51f7d124af 100644 --- a/quickfixj-core/src/main/java/quickfix/JdbcLog.java +++ b/quickfixj-core/src/main/java/quickfix/JdbcLog.java @@ -55,7 +55,7 @@ class JdbcLog extends AbstractLog { private final Map deleteItemsSqlCache = new HashMap<>(); public JdbcLog(SessionSettings settings, SessionID sessionID, DataSource ds) - throws SQLException, ClassNotFoundException, ConfigError, FieldConvertError { + throws SQLException, ConfigError, FieldConvertError { this.sessionID = sessionID; dataSource = ds == null ? JdbcUtil.getDataSource(settings, sessionID) @@ -109,8 +109,8 @@ private void createCachedSql() { } private void createInsertItemSql(String tableName) { - insertItemSqlCache.put(tableName, "INSERT INTO " + tableName + " (time, " - + getIDColumns(extendedSessionIdSupported) + ", text) " + "VALUES (?," + insertItemSqlCache.put(tableName, "INSERT INTO " + tableName + " (time," + + getIDColumns(extendedSessionIdSupported) + ",text) " + "VALUES (?," + getIDPlaceholders(extendedSessionIdSupported) + ",?)"); } diff --git a/quickfixj-core/src/main/java/quickfix/JdbcSetting.java b/quickfixj-core/src/main/java/quickfix/JdbcSetting.java index 59c31d68e9..75df8aa74e 100644 --- a/quickfixj-core/src/main/java/quickfix/JdbcSetting.java +++ b/quickfixj-core/src/main/java/quickfix/JdbcSetting.java @@ -19,6 +19,8 @@ package quickfix; +import javax.sql.DataSource; + /** * Class for storing JDBC setting constants shared by both the log and message * store classes. @@ -103,40 +105,62 @@ public class JdbcSetting { public static final String SETTING_JDBC_SESSION_ID_DEFAULT_PROPERTY_VALUE = "JdbcSessionIdDefaultPropertyValue"; /** - * Specifies the maximum number of connections to the database - * - * @see http://proxool.sourceforge.net/properties.html + * 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. + * A reasonable value for this is best determined by your execution environment. When the pool reaches this size, + * and no idle connections are available, calls to {@link DataSource#getConnection()} will block for up to + * {@link JdbcSetting#SETTING_JDBC_CONNECTION_TIMEOUT} milliseconds before timing out. */ public static final String SETTING_JDBC_MAX_ACTIVE_CONNECTION = "JdbcMaxActiveConnection"; /** - * Specifies if the housekeeper comes across a thread that has been active for longer than - * this then it will kill it. So make sure you set this to a number bigger than your - * slowest expected response! - * - * @see http://proxool.sourceforge.net/properties.html + * Controls the minimum number of idle connections that HikariCP tries to maintain in the pool. + * If the idle connections dip below this value and total connections in the pool are less than + * {@link JdbcSetting#SETTING_JDBC_MAX_ACTIVE_CONNECTION}, HikariCP will make the best effort to add + * additional connections quickly and efficiently. However, for maximum performance and responsiveness + * to spike demands, we recommend not setting this value and instead allowing HikariCP to act as a fixed + * size connection pool. */ - public static final String SETTING_JDBC_MAX_ACTIVE_TIME = "JdbcMaxActiveTime"; + public static final String SETTING_JDBC_MIN_IDLE_CONNECTION = "JdbcMinIdleConnection"; /** - * Specifies the maximum amount of time that a connection exists for before - * it is killed (milliseconds). - * - * @see http://proxool.sourceforge.net/properties.html + * Controls the maximum lifetime of a connection in the pool. An in-use connection will never be retired, only when + * it is closed will it then be removed. On a connection-by-connection basis, minor negative attenuation is applied to + * avoid mass-extinction in the pool. We strongly recommend setting this value, and it should be several seconds shorter + * than any database or infrastructure imposed connection time limit. A value of 0 indicates no maximum lifetime (infinite + * lifetime), subject of course to the {@link JdbcSetting#SETTING_JDBC_CONNECTION_IDLE_TIMEOUT} setting. */ public static final String SETTING_JDBC_MAX_CONNECTION_LIFETIME = "JdbcMaxConnectionLifeTime"; /** - * Specifies the maximum number of connections we can be building at any one time. - * That is, the number of new connections that have been requested but aren't yet - * available for use. Because connections can be built using more than one thread - * (for instance, when they are built on demand) and it takes a finite time between - * deciding to build the connection and it becoming available we need some way of - * ensuring that a lot of threads don't all decide to build a connection at once. - * (We could solve this in a smarter way - and indeed we will one day) - * - * @see http://proxool.sourceforge.net/properties.html + * Controls the maximum number of milliseconds that a client (that's you) will wait for a connection from the pool. If this time + * is exceeded without a connection becoming available, a SQLException will be thrown. Lowest acceptable connection timeout is 250 ms. */ - public static final String SETTING_JDBC_SIMULTANEOUS_BUILD_THROTTLE = "JdbcSimultaneousBuildThrottle"; + public static final String SETTING_JDBC_CONNECTION_TIMEOUT = "JdbcConnectionTimeout"; + /** + * Controls the maximum amount of time that a connection is allowed to sit idle in the pool. + * This setting only applies when {@link JdbcSetting#SETTING_JDBC_MIN_IDLE_CONNECTION} is defined to be less than + * {@link JdbcSetting#SETTING_JDBC_MAX_ACTIVE_CONNECTION}. Idle connections will not be retired once the pool + * reaches {@link JdbcSetting#SETTING_JDBC_MIN_IDLE_CONNECTION} connections. + */ + public static final String SETTING_JDBC_CONNECTION_IDLE_TIMEOUT = "JdbcConnectionIdleTimeout"; + + /** + * Controls how frequently HikariCP will attempt to keep a connection alive, in order to prevent it from being timed out by the + * database or network infrastructure. This value must be less than the {@link JdbcSetting#SETTING_JDBC_MAX_CONNECTION_LIFETIME} value. + * A "keepalive" will only occur on an idle connection. When the time arrives for a "keepalive" against a given connection, that connection + * will be removed from the pool, "pinged", and then returned to the pool. The 'ping' is one of either: invocation of the JDBC4 + * {@link java.sql.Connection#isValid(int)} method, or execution of the {@link JdbcSetting#SETTING_JDBC_CONNECTION_TEST_QUERY}. Typically, the duration + * out-of-the-pool should be measured in single digit milliseconds or even sub-millisecond, and therefore should have little or no noticeable + * performance impact. The minimum allowed value is 30000ms (30 seconds), but a value in the range of minutes is most desirable. + */ + public static final String SETTING_JDBC_CONNECTION_KEEPALIVE_TIME = "JdbcConnectionKeepaliveTime"; + + /** + * If your driver supports JDBC4 we strongly recommend not setting this property. This is for "legacy" drivers that do not support the + * JDBC4 {@link java.sql.Connection#isValid(int)} API. This is the query that will be executed just before a connection is given to you + * from the pool to validate that the connection to the database is still alive. + */ + public static final String SETTING_JDBC_CONNECTION_TEST_QUERY = "JdbcConnectionTestQuery"; } diff --git a/quickfixj-core/src/main/java/quickfix/JdbcUtil.java b/quickfixj-core/src/main/java/quickfix/JdbcUtil.java index 14ffc259e1..c6ce2c2fda 100644 --- a/quickfixj-core/src/main/java/quickfix/JdbcUtil.java +++ b/quickfixj-core/src/main/java/quickfix/JdbcUtil.java @@ -19,7 +19,9 @@ package quickfix; -import org.logicalcobwebs.proxool.ProxoolDataSource; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; import javax.naming.InitialContext; import javax.naming.NamingException; @@ -31,89 +33,96 @@ import java.sql.SQLException; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Function; class JdbcUtil { static final String CONNECTION_POOL_ALIAS = "quickfixj"; + static final int DEFAULT_MAX_CONNECTION_COUNT = 32; + static final long DEFAULT_MAX_CONNECTION_LIFETIME = TimeUnit.HOURS.toMillis(8); + static final long DEFAULT_CONNECTION_TIMEOUT = 250L; + static final long DEFAULT_CONNECTION_IDLE_TIMEOUT = TimeUnit.MINUTES.toMillis(10); + static final long DEFAULT_CONNECTION_KEEPALIVE_TIME = 0; - private static final Map dataSources = new ConcurrentHashMap<>(); + private static final Map dataSources = new ConcurrentHashMap<>(); private static final AtomicInteger dataSourceCounter = new AtomicInteger(); - static DataSource getDataSource(SessionSettings settings, SessionID sessionID) - throws ConfigError, FieldConvertError { + static DataSource getDataSource(SessionSettings settings, SessionID sessionID) throws ConfigError, FieldConvertError { if (settings.isSetting(sessionID, JdbcSetting.SETTING_JDBC_DS_NAME)) { - String jndiName = settings.getString(sessionID, JdbcSetting.SETTING_JDBC_DS_NAME); - try { - return (DataSource) new InitialContext().lookup(jndiName); - } catch (NamingException e) { - throw new ConfigError(e); - } + return getJNDIDataSource(settings, sessionID); } else { - String jdbcDriver = settings.getString(sessionID, JdbcSetting.SETTING_JDBC_DRIVER); - String connectionURL = settings.getString(sessionID, - JdbcSetting.SETTING_JDBC_CONNECTION_URL); - String user = settings.getString(sessionID, JdbcSetting.SETTING_JDBC_USER); - String password = settings.getString(sessionID, JdbcSetting.SETTING_JDBC_PASSWORD); - int maxConnCount = settings - .isSetting(JdbcSetting.SETTING_JDBC_MAX_ACTIVE_CONNECTION) ? - settings.getInt(sessionID, JdbcSetting.SETTING_JDBC_MAX_ACTIVE_CONNECTION) : - 32; - int simultaneousBuildThrottle = settings - .isSetting(JdbcSetting.SETTING_JDBC_SIMULTANEOUS_BUILD_THROTTLE) ? - settings.getInt(sessionID, JdbcSetting.SETTING_JDBC_SIMULTANEOUS_BUILD_THROTTLE) : - maxConnCount; - long maxActiveTime = settings - .isSetting(JdbcSetting.SETTING_JDBC_MAX_ACTIVE_TIME) ? - settings.getLong(sessionID, JdbcSetting.SETTING_JDBC_MAX_ACTIVE_TIME) : - 5000; - int maxConnLifetime = settings - .isSetting(JdbcSetting.SETTING_JDBC_MAX_CONNECTION_LIFETIME) ? - settings.getInt(sessionID, JdbcSetting.SETTING_JDBC_MAX_CONNECTION_LIFETIME) : - 28800000; - - return getDataSource(jdbcDriver, connectionURL, user, password, true, maxConnCount, - simultaneousBuildThrottle, maxActiveTime, maxConnLifetime); + return getOrCreatePooledDataSource(settings, sessionID); + } + } + + private static DataSource getJNDIDataSource(SessionSettings settings, SessionID sessionID) throws ConfigError { + String jndiName = settings.getString(sessionID, JdbcSetting.SETTING_JDBC_DS_NAME); + try { + return (DataSource) new InitialContext().lookup(jndiName); + } catch (NamingException e) { + throw new ConfigError(e); } } - static DataSource getDataSource(String jdbcDriver, String connectionURL, String user, String password, boolean cache) { - return getDataSource(jdbcDriver, connectionURL, user, password, cache, 10, 10, 5000, 28800000); + private static DataSource getOrCreatePooledDataSource(SessionSettings settings, SessionID sessionID) throws ConfigError, FieldConvertError { + String jdbcDriver = settings.getString(sessionID, JdbcSetting.SETTING_JDBC_DRIVER); + String connectionURL = settings.getString(sessionID,JdbcSetting.SETTING_JDBC_CONNECTION_URL); + String user = settings.getString(sessionID, JdbcSetting.SETTING_JDBC_USER); + String password = settings.getString(sessionID, JdbcSetting.SETTING_JDBC_PASSWORD); + return getOrCreatePooledDataSource(settings, sessionID, jdbcDriver, connectionURL, user, password); } - /** - * This is typically called from a single thread, but just in case we are using an atomic loading function - * to avoid the creation of two data sources simultaneously. The cache itself is thread safe. - */ - static DataSource getDataSource(String jdbcDriver, String connectionURL, String user, String password, - boolean cache, int maxConnCount, int simultaneousBuildThrottle, - long maxActiveTime, int maxConnLifetime) { + static DataSource getOrCreatePooledDataSource(SessionSettings settings, SessionID sessionID, String jdbcDriver, String connectionURL, String user, String password) + throws ConfigError, FieldConvertError { String key = jdbcDriver + "#" + connectionURL + "#" + user + "#" + password; - ProxoolDataSource ds = cache ? dataSources.get(key) : null; - - if (ds == null) { - final Function loadingFunction = dataSourceKey -> { - final ProxoolDataSource dataSource = new ProxoolDataSource(CONNECTION_POOL_ALIAS + "-" + dataSourceCounter.incrementAndGet()); - - dataSource.setDriver(jdbcDriver); - dataSource.setDriverUrl(connectionURL); - - // Bug in Proxool 0.9RC2. Must set both delegate properties and individual setters. :-( - dataSource.setDelegateProperties("user=" + user + "," - + (password != null && !"".equals(password) ? "password=" + password : "")); - dataSource.setUser(user); - dataSource.setPassword(password); - - dataSource.setMaximumActiveTime(maxActiveTime); - dataSource.setMaximumConnectionLifetime(maxConnLifetime); - dataSource.setMaximumConnectionCount(maxConnCount); - dataSource.setSimultaneousBuildThrottle(simultaneousBuildThrottle); - return dataSource; - }; - ds = cache ? dataSources.computeIfAbsent(key, loadingFunction) : loadingFunction.apply(key); + + HikariDataSource dataSource = dataSources.get(key); + + if (dataSource != null) { + return dataSource; + } + + HikariDataSource newDataSource = createPooledDataSource(settings, sessionID, jdbcDriver, connectionURL, user, password); + + if (dataSources.putIfAbsent(key, newDataSource) == null) { + return newDataSource; + } else { + return dataSources.get(key); } - return ds; + } + + private static HikariDataSource createPooledDataSource(SessionSettings settings, SessionID sessionID, String jdbcDriver, String connectionURL, String user, String password) + throws ConfigError, FieldConvertError { + HikariConfig configuration = new HikariConfig(); + configuration.setPoolName(CONNECTION_POOL_ALIAS + "-" + dataSourceCounter.incrementAndGet()); + configuration.setDriverClassName(jdbcDriver); + configuration.setJdbcUrl(connectionURL); + configuration.setUsername(user); + configuration.setPassword(password); + + int maxConnectionCount = settings.getIntOrDefault(sessionID, JdbcSetting.SETTING_JDBC_MAX_ACTIVE_CONNECTION, DEFAULT_MAX_CONNECTION_COUNT); + configuration.setMaximumPoolSize(maxConnectionCount); + + int minIdleConnectionCount = settings.getIntOrDefault(sessionID, JdbcSetting.SETTING_JDBC_MIN_IDLE_CONNECTION, maxConnectionCount); + configuration.setMinimumIdle(minIdleConnectionCount); + + long maxConnectionLifetime = settings.getLongOrDefault(sessionID, JdbcSetting.SETTING_JDBC_MAX_CONNECTION_LIFETIME, DEFAULT_MAX_CONNECTION_LIFETIME); + configuration.setMaxLifetime(maxConnectionLifetime); + + long connectionTimeout = settings.getLongOrDefault(sessionID, JdbcSetting.SETTING_JDBC_CONNECTION_TIMEOUT, DEFAULT_CONNECTION_TIMEOUT); + configuration.setConnectionTimeout(connectionTimeout); + + long connectionIdleTimeout = settings.getLongOrDefault(sessionID, JdbcSetting.SETTING_JDBC_CONNECTION_IDLE_TIMEOUT, DEFAULT_CONNECTION_IDLE_TIMEOUT); + configuration.setIdleTimeout(connectionIdleTimeout); + + long connectionKeepaliveTime = settings.getLongOrDefault(sessionID, JdbcSetting.SETTING_JDBC_CONNECTION_KEEPALIVE_TIME, DEFAULT_CONNECTION_KEEPALIVE_TIME); + configuration.setKeepaliveTime(connectionKeepaliveTime); + + String connectionTestQuery = settings.getStringOrDefault(sessionID, JdbcSetting.SETTING_JDBC_CONNECTION_TEST_QUERY, null); + configuration.setConnectionTestQuery(connectionTestQuery); + + return new HikariDataSource(configuration); } static void close(SessionID sessionID, Connection connection) { @@ -165,7 +174,7 @@ private static boolean isColumn(DatabaseMetaData metaData, String tableName, Str static String getIDWhereClause(boolean isExtendedSessionID) { return isExtendedSessionID ? ("beginstring=? and sendercompid=? and sendersubid=? and senderlocid=? and " - + "targetcompid=? and targetsubid=? and targetlocid=? and session_qualifier=? ") + + "targetcompid=? and targetsubid=? and targetlocid=? and session_qualifier=? ") : "beginstring=? and sendercompid=? and targetcompid=? and session_qualifier=? "; } @@ -201,5 +210,4 @@ static int setSessionIdParameters(SessionID sessionID, PreparedStatement query, private static String getSqlValue(String javaValue, String defaultSqlValue) { return !SessionID.NOT_SET.equals(javaValue) ? javaValue : defaultSqlValue; } - } diff --git a/quickfixj-core/src/main/java/quickfix/SessionSettings.java b/quickfixj-core/src/main/java/quickfix/SessionSettings.java index 319e44e1d2..3765c35c1f 100644 --- a/quickfixj-core/src/main/java/quickfix/SessionSettings.java +++ b/quickfixj-core/src/main/java/quickfix/SessionSettings.java @@ -186,6 +186,13 @@ public String getString(String key) throws ConfigError { return getString(DEFAULT_SESSION_ID, key); } + /** + * Gets a string from the default section if present or use default value. + */ + public String getStringOrDefault(String key, String defaultValue) throws ConfigError { + return isSetting(key) ? getString(key) : defaultValue; + } + /** * Get a settings string. * @@ -202,6 +209,13 @@ public String getString(SessionID sessionID, String key) throws ConfigError { return value; } + /** + * Get a settings string if present or use default value. + */ + public String getStringOrDefault(SessionID sessionID, String key, String defaultValue) throws ConfigError { + return isSetting(sessionID, key) ? getString(sessionID, key) : defaultValue; + } + /** * Return the settings for a session as a Properties object. * @@ -265,6 +279,13 @@ public long getLong(String key) throws ConfigError, FieldConvertError { return getLong(DEFAULT_SESSION_ID, key); } + /** + * Gets a long from the default section of settings if present or use default value. + */ + public long getLongOrDefault(String key, long defaultValue) throws ConfigError, FieldConvertError { + return isSetting(key) ? getLong(key) : defaultValue; + } + /** * Get a settings value as a long integer. * @@ -282,6 +303,13 @@ public long getLong(SessionID sessionID, String key) throws ConfigError, FieldCo } } + /** + * Get an existing settings value as a long if present or use default value. + */ + public long getLongOrDefault(SessionID sessionID, String key, long defaultValue) throws ConfigError, FieldConvertError { + return isSetting(sessionID, key) ? getLong(sessionID, key) : defaultValue; + } + /** * Gets an int from the default section of settings. * @@ -294,6 +322,13 @@ public int getInt(String key) throws ConfigError, FieldConvertError { return getInt(DEFAULT_SESSION_ID, key); } + /** + * Gets an int from the default section of settings if present or use default value. + */ + public int getIntOrDefault(String key, int defaultValue) throws ConfigError, FieldConvertError { + return isSetting(key) ? getInt(key) : defaultValue; + } + /** * Get a settings value as an integer. * @@ -311,6 +346,13 @@ public int getInt(SessionID sessionID, String key) throws ConfigError, FieldConv } } + /** + * Get an existing settings value as an integer if present or use default value. + */ + public int getIntOrDefault(SessionID sessionID, String key, int defaultValue) throws ConfigError, FieldConvertError { + return isSetting(sessionID, key) ? getInt(sessionID, key) : defaultValue; + } + private Properties getOrCreateSessionProperties(SessionID sessionID) { return sections.computeIfAbsent(sessionID, k -> new Properties(sections.get(DEFAULT_SESSION_ID))); } diff --git a/quickfixj-core/src/test/java/quickfix/JdbcLogTest.java b/quickfixj-core/src/test/java/quickfix/JdbcLogTest.java index 9c1d5147b2..189f9e1449 100644 --- a/quickfixj-core/src/test/java/quickfix/JdbcLogTest.java +++ b/quickfixj-core/src/test/java/quickfix/JdbcLogTest.java @@ -127,16 +127,17 @@ private void dropTable(String tableName) throws SQLException { private void setUpJdbcLog(boolean filterHeartbeats, DataSource dataSource) throws ClassNotFoundException, SQLException, ConfigError { connection = JdbcTestSupport.getConnection(); + long now = System.currentTimeMillis(); + sessionID = new SessionID("FIX.4.2", "SENDER-" + now, "TARGET-" + now); SessionSettings settings = new SessionSettings(); if (filterHeartbeats) { settings.setBool(JdbcSetting.SETTING_JDBC_LOG_HEARTBEATS, false); } + settings.setString(sessionID, JdbcSetting.SETTING_JDBC_CONNECTION_TEST_QUERY, "SELECT COUNT(1) FROM INFORMATION_SCHEMA.SYSTEM_USERS WHERE 1 = 0;"); JdbcTestSupport.setHypersonicSettings(settings); initializeTableDefinitions(connection); logFactory = new JdbcLogFactory(settings); logFactory.setDataSource(dataSource); - long now = System.currentTimeMillis(); - sessionID = new SessionID("FIX.4.2", "SENDER-" + now, "TARGET-" + now); settings.setString(sessionID, "ConnectionType", "acceptor"); log = (JdbcLog) logFactory.create(sessionID); assertEquals(0, getRowCount(connection, log.getIncomingMessagesTableName())); diff --git a/quickfixj-core/src/test/java/quickfix/JdbcStoreLegacyTest.java b/quickfixj-core/src/test/java/quickfix/JdbcStoreLegacyTest.java index 49e1622787..fc38ce7c5d 100644 --- a/quickfixj-core/src/test/java/quickfix/JdbcStoreLegacyTest.java +++ b/quickfixj-core/src/test/java/quickfix/JdbcStoreLegacyTest.java @@ -33,7 +33,7 @@ protected void initializeTableDefinitions(String sessionsTableName, String messa throws ConfigError, SQLException, IOException { Connection connection = null; try { - connection = getDataSource().getConnection(); + connection = getTestDataSource().getConnection(); JdbcTestSupport.loadSQL(connection, "config/sql/hsqldb/messages_table.sql", new JdbcTestSupport.HypersonicLegacyPreprocessor(messagesTableName)); diff --git a/quickfixj-core/src/test/java/quickfix/JdbcStoreTest.java b/quickfixj-core/src/test/java/quickfix/JdbcStoreTest.java index 647e595224..b3fe5f93cb 100644 --- a/quickfixj-core/src/test/java/quickfix/JdbcStoreTest.java +++ b/quickfixj-core/src/test/java/quickfix/JdbcStoreTest.java @@ -54,7 +54,7 @@ protected void setUp() throws Exception { } protected void tearDown() throws Exception { - assertNoActiveConnections(); + assertNoActiveConnections(getTestDataSource()); if (initialContextFactory != null) { System.setProperty(Context.INITIAL_CONTEXT_FACTORY, initialContextFactory); } @@ -62,7 +62,7 @@ protected void tearDown() throws Exception { } private void bindDataSource() throws NamingException { - new InitialContext().rebind("TestDataSource", getDataSource()); + new InitialContext().rebind("TestDataSource", getTestDataSource()); } protected MessageStoreFactory getMessageStoreFactory() throws ConfigError, SQLException, @@ -91,7 +91,7 @@ private JdbcStoreFactory getMessageStoreFactory(String sessionTableName, String public void testExplicitDataSource() throws Exception { // No JNDI data source name is set up here JdbcStoreFactory factory = new JdbcStoreFactory(new SessionSettings()); - factory.setDataSource(getDataSource()); + factory.setDataSource(getTestDataSource()); factory.create(new SessionID("FIX4.4", "SENDER", "TARGET")); } @@ -124,7 +124,7 @@ protected void initializeTableDefinitions(String sessionsTableName, String messa throws ConfigError, SQLException, IOException { Connection connection = null; try { - connection = getDataSource().getConnection(); + connection = getTestDataSource().getConnection(); if (messagesTableName != null) { dropTable(connection, messagesTableName); } @@ -140,8 +140,8 @@ protected void initializeTableDefinitions(String sessionsTableName, String messa } } - protected DataSource getDataSource() { - return JdbcUtil.getDataSource(HSQL_DRIVER, HSQL_CONNECTION_URL, HSQL_USER, "", true); + protected DataSource getTestDataSource() { + return JdbcTestSupport.getTestDataSource(HSQL_DRIVER, HSQL_CONNECTION_URL, HSQL_USER, ""); } public void testCreationTime() throws Exception { diff --git a/quickfixj-core/src/test/java/quickfix/JdbcTestSupport.java b/quickfixj-core/src/test/java/quickfix/JdbcTestSupport.java index a21357af25..a2f573f472 100644 --- a/quickfixj-core/src/test/java/quickfix/JdbcTestSupport.java +++ b/quickfixj-core/src/test/java/quickfix/JdbcTestSupport.java @@ -19,6 +19,9 @@ package quickfix; +import com.zaxxer.hikari.HikariDataSource; + +import javax.sql.DataSource; import java.io.IOException; import java.io.InputStream; import java.sql.Connection; @@ -26,13 +29,11 @@ import java.sql.SQLException; import java.sql.Statement; -import org.junit.Assert; - -import org.logicalcobwebs.proxool.ProxoolException; -import org.logicalcobwebs.proxool.ProxoolFacade; -import org.logicalcobwebs.proxool.admin.SnapshotIF; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; public class JdbcTestSupport { + public static final String HSQL_DRIVER = "org.hsqldb.jdbcDriver"; public static final String HSQL_CONNECTION_URL = "jdbc:hsqldb:mem:quickfixj"; public static final String HSQL_USER = "sa"; @@ -102,7 +103,7 @@ public static void dropTable(Connection connection, String tableName) throws SQL execSQL(connection, "drop table " + tableName + " if exists"); } - public static void execSQL(Connection connection, String sql) throws SQLException, IOException { + public static void execSQL(Connection connection, String sql) throws SQLException { Statement stmt = connection.createStatement(); stmt.execute(sql); stmt.close(); @@ -115,12 +116,24 @@ private static String getString(InputStream in) throws IOException { return new String(b); } - static void assertNoActiveConnections() throws ProxoolException { - for (String alias : ProxoolFacade.getAliases()) { - SnapshotIF snapshot = ProxoolFacade.getSnapshot(alias, true); - Assert.assertEquals("unclosed connections: " + alias, 0, snapshot - .getActiveConnectionCount()); - } + static void assertNoActiveConnections(DataSource dataSource) { + assertTrue(dataSource instanceof HikariDataSource); + + HikariDataSource hikariDataSource = (HikariDataSource) dataSource; + assertEquals("Some connections are still alive", 0, hikariDataSource.getHikariPoolMXBean().getActiveConnections()); } + static DataSource getTestDataSource(String jdbcDriver, String connectionURL, String user, String password) { + SessionID sessionID = new SessionID("TEST", "", ""); + + SessionSettings settings = new SessionSettings(); + // HSQL doesn't support JDBC4 which means that test query has to be supplied to HikariCP + settings.setString(sessionID, JdbcSetting.SETTING_JDBC_CONNECTION_TEST_QUERY, "SELECT COUNT(1) FROM INFORMATION_SCHEMA.SYSTEM_USERS WHERE 1 = 0;"); + + try { + return JdbcUtil.getOrCreatePooledDataSource(settings, sessionID, jdbcDriver, connectionURL, user, password); + } catch (ConfigError | FieldConvertError e) { + throw new RuntimeException("Unable to get or create pooled data source", e); + } + } } diff --git a/quickfixj-core/src/test/java/quickfix/SessionSettingsTest.java b/quickfixj-core/src/test/java/quickfix/SessionSettingsTest.java index c6b5435f3f..2de5d1ad10 100644 --- a/quickfixj-core/src/test/java/quickfix/SessionSettingsTest.java +++ b/quickfixj-core/src/test/java/quickfix/SessionSettingsTest.java @@ -232,6 +232,17 @@ public void testDefaultsSet() throws Exception { assertEquals("bargle", settings.getString("FileStorePath")); } + @Test + public void testMissingValues() throws ConfigError, FieldConvertError { + final SessionSettings settings = new SessionSettings(); + assertEquals("1", settings.getStringOrDefault("a", "1")); + assertEquals("2", settings.getStringOrDefault("b", "2")); + assertEquals(3, settings.getIntOrDefault("c", 3)); + assertEquals(4, settings.getIntOrDefault("d", 4)); + assertEquals(5L, settings.getLongOrDefault("e", 5L)); + assertEquals(6L, settings.getLongOrDefault("f", 6L)); + } + @Test public void testSpecialCharactersInKeys() throws Exception { final SessionSettings settings = setUpSession("$$$foo bar.baz@@@=value\n"); diff --git a/quickfixj-core/src/test/java/quickfix/test/acceptance/AcceptanceTestSuite.java b/quickfixj-core/src/test/java/quickfix/test/acceptance/AcceptanceTestSuite.java index 72317fb7bd..30e3b91461 100644 --- a/quickfixj-core/src/test/java/quickfix/test/acceptance/AcceptanceTestSuite.java +++ b/quickfixj-core/src/test/java/quickfix/test/acceptance/AcceptanceTestSuite.java @@ -7,9 +7,6 @@ import junit.framework.TestResult; import junit.framework.TestSuite; import org.apache.mina.util.AvailablePortFinder; -import org.logicalcobwebs.proxool.ProxoolException; -import org.logicalcobwebs.proxool.ProxoolFacade; -import org.logicalcobwebs.proxool.admin.SnapshotIF; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import quickfix.Session; @@ -93,20 +90,6 @@ public void run(TestResult result) { //printDatabasePoolingStatistics(); } - @SuppressWarnings("unused") - protected void printDatabasePoolingStatistics() { - try { - for (String alias : ProxoolFacade.getAliases()) { - SnapshotIF snapshot = ProxoolFacade.getSnapshot(alias, true); - System.out.println("active:" + snapshot.getActiveConnectionCount() + ",max:" - + snapshot.getMaximumConnectionCount() + ",served:" - + snapshot.getServedCount()); - } - } catch (ProxoolException e) { - e.printStackTrace(); - } - } - private List load(String filename) throws IOException { ArrayList steps = new ArrayList<>(); log.info("load test: " + filename);