diff --git a/bin/testfiles/JDBC_TESTS.jmx b/bin/testfiles/JDBC_TESTS.jmx
index ff1705a823d..66277d3bf8a 100644
--- a/bin/testfiles/JDBC_TESTS.jmx
+++ b/bin/testfiles/JDBC_TESTS.jmx
@@ -8,7 +8,7 @@
- ../lib/opt/hsqldb-2.4.0.jar
+ ../lib/opt/hsqldb-2.7.4.jar
diff --git a/gradle/verification-keyring.keys b/gradle/verification-keyring.keys
index bffc6702e3a..20e89e98240 100644
--- a/gradle/verification-keyring.keys
+++ b/gradle/verification-keyring.keys
@@ -6813,6 +6813,34 @@ Dw==
=QOGP
-----END PGP PUBLIC KEY BLOCK-----
+pub 4CC08E7F47C3EC76
+uid Brett Wooldridge (Sonatype)
+
+sub 4D3FB07DD9F19B56
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQENBFI4+QsBCAC1+xCdhXj0MuViNA21Pno8bHunP0UIZQmC9BNl7dVuUC8/rg9V
+1dlZyXF8SNycOKqT461m41H5VNBmwt64OIuJiUcUreVSs07iLzpn8mQPwyTaRZYd
+YlZns5V/3ukfGYUVCWScMdc8WaJwTVxfRwNhnJ2/QbAcIZDypwAd0P03ofpvnt/8
+YqvVyzpJqNTDLFjLpEcditVn4EioVVMcvUu4YVwmUSdBjrMLp0xC8PvbyWiw4dCA
+T4C2zFycrr4M2legiZv/N6Tw0fPRE/EALYtIhgLSeqx5Pg9ku/KA6zMraFovnMDM
+haJ9+ZsCPxd9JaJ021C9I7EXDA+W5t+DPODDABEBAAG0OEJyZXR0IFdvb2xkcmlk
+Z2UgKFNvbmF0eXBlKSA8YnJldHQud29vbGRyaWRnZUBnbWFpbC5jb20+uQENBFI4
++QsBCAD0Xrq6nXmqubsB+XdgLof14wH7UIw693FUUndKcK+LVaMe7dP8F1Emkorf
+YvwTOKQy6L+rUOm23MHuxxwB+msIpMX3WCzFGSq1WjYPd6wVj47yP/wqqhIqQO3q
+tUeOVlyTwy8KccrAkXpDjkTlZ0cVP2lqNo6gRTypkvmHYgLYzNNV1GJm+v+t4sm4
+jMepvKLl11/gUNLHx2VpL37w1i6Mm53iiW2GXGin1gPSWB3FtspMLAQdE2Xk0yRk
+s+eUJ5e8oj3eJD5w4b3fqsWFCmK8q+/5uPK3Po2xe1oSmpHBm38MFUAxErtabNrB
+EioSC5wNER0DhB4gEKVUXLyIDXUDABEBAAGJAR8EGAECAAkFAlI4+QsCGwwACgkQ
+TMCOf0fD7HYHrgf9F4p3sWyNApGF8V1GEQEyGUPtfxNd0N1Jzd10MCh470fiBjaQ
+IebkCEhneiNBAOWK52HxAlWRINAAa7fVJ8XpmzTbTLDLL1ZrEI//21ZQ3gUxCRgr
+shy7lEV1fNJTuZadBbOU/+1L2Bbz9zu7Vju+9DU5sr3c5Byu/E+l/o1i/DvymoFA
+OOepAO4IxpEV/ma4d8KYaWhSSb7UgwMjAkPt3YsnxQAnNbApZenVo+NLO+2XRAbd
+BzEtRoazqYje0BzSewiTyXruVKK7Ineo6tRWyOMvdYhnoz5EZejp3gkaBmE4U0aa
+qOSJ1UKXf8amAyQGcdIzRNx+3apFHn0h2yaMug==
+=mIr1
+-----END PGP PUBLIC KEY BLOCK-----
+
pub 4E066E0459CD109B
uid Henri Biestro (CODE SIGNING KEY)
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index d46bb6a739c..a3193356667 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -294,6 +294,7 @@
+
diff --git a/lib/aareadme.txt b/lib/aareadme.txt
index 96315b8a9e3..ce2e541d979 100644
--- a/lib/aareadme.txt
+++ b/lib/aareadme.txt
@@ -244,10 +244,6 @@ tika-1.21
http://tika.apache.org/
- Regular Expression Extractor
-commons-dbcp2-2.5.0 (org.apache.commons.dbcp2)
---------------------------
-- DataSourceElement (JDBC)
-
Saxon-HE-9.9.1-5 (net.sf.saxon)
--------------------------
- XPath2Extractor (XML)
diff --git a/src/bom-thirdparty/build.gradle.kts b/src/bom-thirdparty/build.gradle.kts
index f26a2b6d191..ca22d231c3d 100644
--- a/src/bom-thirdparty/build.gradle.kts
+++ b/src/bom-thirdparty/build.gradle.kts
@@ -59,6 +59,7 @@ dependencies {
api("com.miglayout:miglayout-swing:5.3")
api("com.sun.activation:javax.activation:1.2.0")
api("com.thoughtworks.xstream:xstream:1.4.21")
+ api("com.zaxxer:HikariCP:7.0.2")
api("commons-codec:commons-codec:1.19.0")
api("commons-collections:commons-collections:3.2.2")
api("commons-io:commons-io:2.20.0")
@@ -90,7 +91,6 @@ dependencies {
api("net.sf.saxon:Saxon-HE:12.9")
api("org.apache-extras.beanshell:bsh:2.0b6")
api("org.apache.commons:commons-collections4:4.5.0")
- api("org.apache.commons:commons-dbcp2:2.9.0")
api("org.apache.commons:commons-jexl3:3.5.0")
api("org.apache.commons:commons-jexl:2.1.1")
api("org.apache.commons:commons-lang3:3.19.0") {
diff --git a/src/dist-check/build.gradle.kts b/src/dist-check/build.gradle.kts
index 00d5edb3961..18a5eb11a5f 100644
--- a/src/dist-check/build.gradle.kts
+++ b/src/dist-check/build.gradle.kts
@@ -44,7 +44,7 @@ dependencies {
extraTestDependencies(platform(projects.src.bomThirdparty))
extraTestDependencies(platform(projects.src.bomTesting))
- extraTestDependencies("org.hsqldb:hsqldb::jdk8")
+ extraTestDependencies("org.hsqldb:hsqldb")
extraTestDependencies("org.apache.mina:mina-core")
extraTestDependencies("org.apache.ftpserver:ftplet-api")
extraTestDependencies("org.apache.ftpserver:ftpserver-core")
diff --git a/src/dist/src/dist/expected_release_jars.csv b/src/dist/src/dist/expected_release_jars.csv
index f7115320585..68a08c083fc 100644
--- a/src/dist/src/dist/expected_release_jars.csv
+++ b/src/dist/src/dist/expected_release_jars.csv
@@ -69,6 +69,7 @@
223993,groovy-xml-5.0.2.jar
126373,hamcrest-3.0.jar
2403,hamcrest-core-3.0.jar
+172312,HikariCP-7.0.2.jar
181512,httpasyncclient-4.1.5.jar
785639,httpclient-4.5.14.jar
327891,httpcore-4.4.16.jar
diff --git a/src/protocol/jdbc/build.gradle.kts b/src/protocol/jdbc/build.gradle.kts
index 2b5521c6ca9..f146f158cce 100644
--- a/src/protocol/jdbc/build.gradle.kts
+++ b/src/protocol/jdbc/build.gradle.kts
@@ -22,7 +22,7 @@ plugins {
dependencies {
api(projects.src.core)
- implementation("org.apache.commons:commons-dbcp2")
+ implementation("com.zaxxer:HikariCP")
implementation("commons-io:commons-io") {
because("IOUtils")
}
diff --git a/src/protocol/jdbc/src/main/java/org/apache/jmeter/protocol/jdbc/AbstractJDBCTestElement.java b/src/protocol/jdbc/src/main/java/org/apache/jmeter/protocol/jdbc/AbstractJDBCTestElement.java
index 57521582185..219d4d0b921 100644
--- a/src/protocol/jdbc/src/main/java/org/apache/jmeter/protocol/jdbc/AbstractJDBCTestElement.java
+++ b/src/protocol/jdbc/src/main/java/org/apache/jmeter/protocol/jdbc/AbstractJDBCTestElement.java
@@ -168,13 +168,11 @@ protected byte[] execute(Connection conn, SampleResult sample) throws SQLExcepti
try (Statement stmt = conn.createStatement()) {
setQueryTimeout(stmt, getIntegerQueryTimeout());
configureMaxRows(stmt);
- ResultSet rs = null;
- try {
- rs = stmt.executeQuery(getQuery());
+ try (ResultSet rs = stmt.executeQuery(getQuery())) {
sample.latencyEnd();
return getStringFromResultSet(rs).getBytes(ENCODING);
} finally {
- close(rs);
+ commitTransaction(conn);
}
}
} else if (CALLABLE.equals(currentQueryType)) {
@@ -186,6 +184,8 @@ protected byte[] execute(Connection conn, SampleResult sample) throws SQLExcepti
sample.latencyEnd();
String sb = resultSetsToString(cstmt,hasResultSet, out);
return sb.getBytes(ENCODING);
+ } finally {
+ commitTransaction(conn);
}
} else if (UPDATE.equals(currentQueryType)) {
try (Statement stmt = conn.createStatement()) {
@@ -195,18 +195,18 @@ protected byte[] execute(Connection conn, SampleResult sample) throws SQLExcepti
int updateCount = stmt.getUpdateCount();
String results = updateCount + " updates";
return results.getBytes(ENCODING);
+ } finally {
+ commitTransaction(conn);
}
} else if (PREPARED_SELECT.equals(currentQueryType)) {
try (PreparedStatement pstmt = getPreparedStatement(conn)) {
setArguments(pstmt);
configureMaxRows(pstmt);
- ResultSet rs = null;
- try {
- rs = pstmt.executeQuery();
+ try (ResultSet rs = pstmt.executeQuery()) {
sample.latencyEnd();
return getStringFromResultSet(rs).getBytes(ENCODING);
} finally {
- close(rs);
+ commitTransaction(conn);
}
}
} else if (PREPARED_UPDATE.equals(currentQueryType)) {
@@ -216,6 +216,8 @@ protected byte[] execute(Connection conn, SampleResult sample) throws SQLExcepti
sample.latencyEnd();
String sb = resultSetsToString(pstmt,false,null);
return sb.getBytes(ENCODING);
+ } finally {
+ commitTransaction(conn);
}
} else if (ROLLBACK.equals(currentQueryType)){
conn.rollback();
@@ -238,6 +240,13 @@ protected byte[] execute(Connection conn, SampleResult sample) throws SQLExcepti
}
}
+ private static void commitTransaction(Connection conn) throws SQLException {
+ if (!conn.getAutoCommit()) {
+ // HikariCP rollsback the transaction when a dirty connection returns back to the pool, so we commit it explicitly
+ conn.commit();
+ }
+ }
+
private void configureMaxRows(Statement stmt) throws SQLException {
int maxRows = getIntegerResultSetMaxRows();
if (maxRows >= 0) {
diff --git a/src/protocol/jdbc/src/main/java/org/apache/jmeter/protocol/jdbc/config/DataSourceElement.java b/src/protocol/jdbc/src/main/java/org/apache/jmeter/protocol/jdbc/config/DataSourceElement.java
index c074c16027f..964e57be20e 100644
--- a/src/protocol/jdbc/src/main/java/org/apache/jmeter/protocol/jdbc/config/DataSourceElement.java
+++ b/src/protocol/jdbc/src/main/java/org/apache/jmeter/protocol/jdbc/config/DataSourceElement.java
@@ -19,14 +19,15 @@
import java.sql.Connection;
import java.sql.SQLException;
-import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
+import java.util.Objects;
+import java.util.Properties;
import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
-import org.apache.commons.dbcp2.BasicDataSource;
import org.apache.jmeter.config.ConfigElement;
import org.apache.jmeter.gui.TestElementMetadata;
import org.apache.jmeter.testbeans.TestBean;
@@ -39,6 +40,8 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import com.zaxxer.hikari.HikariDataSource;
+
@TestElementMetadata(labelResource = "displayName")
public class DataSourceElement extends AbstractTestElement
implements ConfigElement, TestStateListener, TestBean {
@@ -46,6 +49,9 @@ public class DataSourceElement extends AbstractTestElement
private static final long serialVersionUID = 235L;
+ private static final AtomicBoolean VALIDATION_QUERY_USED_WARNING = new AtomicBoolean();
+ private static final AtomicBoolean POOL_PREPARED_STATEMENTS_WARNING = new AtomicBoolean();
+
private transient String dataSource;
private transient String driver;
private transient String dbUrl;
@@ -70,10 +76,10 @@ public class DataSourceElement extends AbstractTestElement
* These are called from different threads, so access must be synchronized.
* The same instance is called in each case.
*/
- private transient BasicDataSource dbcpDataSource;
+ private transient HikariDataSource hikariDataSource;
// Keep a record of the pre-thread pools so that they can be disposed of at the end of a test
- private transient Set perThreadPoolSet;
+ private transient Set perThreadPoolSet;
public DataSourceElement() {
}
@@ -81,23 +87,15 @@ public DataSourceElement() {
@Override
public void testEnded() {
synchronized (this) {
- if (dbcpDataSource != null) {
- try {
- dbcpDataSource.close();
- } catch (SQLException ex) {
- log.error("Error closing pool: {}", getName(), ex);
- }
+ if (hikariDataSource != null) {
+ hikariDataSource.close();
}
- dbcpDataSource = null;
+ hikariDataSource = null;
}
if (perThreadPoolSet != null) {// in case
- for(BasicDataSource dsc : perThreadPoolSet){
+ for(HikariDataSource dsc : perThreadPoolSet){
log.debug("Closing pool: {}@{}", getDataSourceName(), System.identityHashCode(dsc));
- try {
- dsc.close();
- } catch (SQLException ex) {
- log.error("Error closing pool:{}", getName(), ex);
- }
+ dsc.close();
}
perThreadPoolSet=null;
}
@@ -124,10 +122,10 @@ public void testStarted() {
if (maxPool.equals("0")){ // i.e. if we want per thread pooling
variables.putObject(poolName, new DataSourceComponentImpl()); // pool will be created later
} else {
- BasicDataSource src = initPool(maxPool);
+ HikariDataSource src = initPool(maxPool);
synchronized(this){
- dbcpDataSource = src;
- variables.putObject(poolName, new DataSourceComponentImpl(dbcpDataSource));
+ hikariDataSource = src;
+ variables.putObject(poolName, new DataSourceComponentImpl(hikariDataSource));
}
}
}
@@ -142,7 +140,7 @@ public void testStarted(String host) {
public Object clone() {
DataSourceElement el = (DataSourceElement) super.clone();
synchronized (this) {
- el.dbcpDataSource = dbcpDataSource;
+ el.hikariDataSource = hikariDataSource;
el.perThreadPoolSet = perThreadPoolSet;
}
return el;
@@ -208,41 +206,35 @@ public static Connection getConnection(String poolName) throws SQLException{
* Set up the DataSource - maxPool is a parameter, so the same code can
* also be used for setting up the per-thread pools.
*/
- private BasicDataSource initPool(String maxPool) {
- BasicDataSource dataSource = new BasicDataSource();
+ private HikariDataSource initPool(String maxPool) {
+ HikariDataSource dataSource = new HikariDataSource();
if (log.isDebugEnabled()) {
log.debug("MaxPool: {} Timeout: {} TrimInt: {} Auto-Commit: {} Preinit: {} poolPreparedStatements: {}",
maxPool, getTimeout(), getTrimInterval(), isAutocommit(), isPreinit(), poolPreparedStatements);
}
int poolSize = Integer.parseInt(maxPool);
- dataSource.setMinIdle(0);
- dataSource.setInitialSize(poolSize);
- dataSource.setAutoCommitOnReturn(false);
+ dataSource.setMinimumIdle(0);
if (StringUtilities.isNotEmpty(initQuery)) {
String[] sqls = initQuery.split("\n");
- dataSource.setConnectionInitSqls(Arrays.asList(sqls));
- } else {
- dataSource.setConnectionInitSqls(Collections.emptyList());
+ dataSource.setConnectionInitSql(String.join(";", sqls));
}
if (StringUtilities.isNotEmpty(connectionProperties)) {
- dataSource.setConnectionProperties(connectionProperties);
+ dataSource.setDataSourceProperties(parseConnectionProperties(connectionProperties));
}
if (StringUtilities.isNotEmpty(poolPreparedStatements)) {
- int maxPreparedStatements = Integer.parseInt(poolPreparedStatements);
- if (maxPreparedStatements < 0) {
- dataSource.setPoolPreparedStatements(false);
- } else {
- dataSource.setPoolPreparedStatements(true);
- dataSource.setMaxOpenPreparedStatements(10);
+ if (POOL_PREPARED_STATEMENTS_WARNING.compareAndSet(false, true)) {
+ log.warn(
+ "Pool prepared statements required in element \"{}\" in is discouraged." +
+ "Statement cache at the pool level is an anti-pattern, " +
+ "see https://github.com/brettwooldridge/HikariCP?tab=readme-ov-file#statement-cache",
+ getName()
+ );
}
}
- dataSource.setRollbackOnReturn(false);
- dataSource.setMaxIdle(poolSize);
- dataSource.setMaxTotal(poolSize);
- dataSource.setMaxWaitMillis(Long.parseLong(getTimeout()));
-
- dataSource.setDefaultAutoCommit(isAutocommit());
+ dataSource.setMaximumPoolSize(poolSize);
+ dataSource.setConnectionTimeout(Long.parseLong(getTimeout()));
+ dataSource.setAutoCommit(isAutocommit());
if (log.isDebugEnabled()) {
StringBuilder sb = new StringBuilder(40);
@@ -254,26 +246,26 @@ private BasicDataSource initPool(String maxPool) {
sb.append(getCheckQuery());
log.debug(sb.toString());
}
- dataSource.setTestOnBorrow(false);
- dataSource.setTestOnReturn(false);
- dataSource.setTestOnCreate(false);
- dataSource.setTestWhileIdle(false);
- if(isKeepAlive()) {
- dataSource.setTestWhileIdle(true);
+ if (isKeepAlive()) {
+ dataSource.setMaxLifetime(Long.parseLong(getConnectionAge()));
String validationQuery = getCheckQuery();
- if (StringUtilities.isBlank(validationQuery)) {
- dataSource.setValidationQuery(null);
- } else {
- dataSource.setValidationQuery(validationQuery);
+ if (StringUtilities.isNotBlank(validationQuery)) {
+ if (VALIDATION_QUERY_USED_WARNING.compareAndSet(false, true)) {
+ log.warn(
+ "Using explicit connection validation query \"{}\" in element \"{}\" in is discouraged." +
+ "Consider leaving the validation query empty so JDBC's Connection#isValid() would be used for the validation",
+ validationQuery, getName()
+ );
+ }
+ dataSource.setConnectionTestQuery(validationQuery);
}
- dataSource.setSoftMinEvictableIdleTimeMillis(Long.parseLong(getConnectionAge()));
- dataSource.setTimeBetweenEvictionRunsMillis(Integer.parseInt(getTrimInterval()));
+ dataSource.setKeepaliveTime(Long.parseLong(getTrimInterval()));
}
- int transactionIsolation = DataSourceElementBeanInfo.getTransactionIsolationMode(getTransactionIsolation());
- if (transactionIsolation >= 0) {
- dataSource.setDefaultTransactionIsolation(transactionIsolation);
+ String transactionIsolation = getTransactionIsolation();
+ if (!"DEFAULT".equals(transactionIsolation)) {
+ dataSource.setTransactionIsolation(transactionIsolation);
}
String _username = getUsername();
@@ -288,7 +280,7 @@ private BasicDataSource initPool(String maxPool) {
log.debug(sb.toString());
}
dataSource.setDriverClassName(getDriver());
- dataSource.setUrl(getDbUrl());
+ dataSource.setJdbcUrl(getDbUrl());
if (!_username.isEmpty()){
dataSource.setUsername(_username);
@@ -297,7 +289,7 @@ private BasicDataSource initPool(String maxPool) {
if(isPreinit()) {
// side effect - connection pool init - that is what we want
- // see also https://commons.apache.org/proper/commons-dbcp/apidocs/org/apache/commons/dbcp2/BasicDataSource.html#setInitialSize-int-
+ // see also https://commons.apache.org/proper/commons-dbcp/apidocs/org/apache/commons/dbcp2/HikariDataSource.html#setInitialSize-int-
// it says: "The pool is initialized the first time one of the following methods is invoked:
// getConnection, setLogwriter, setLoginTimeout, getLoginTimeout, getLogWriter."
// so we get a connection and close it - which releases it back to the pool (but stays open)
@@ -317,8 +309,29 @@ private BasicDataSource initPool(String maxPool) {
return dataSource;
}
+ private static Properties parseConnectionProperties( String connectionProperties) {
+ Objects.requireNonNull(connectionProperties, "connectionProperties is null");
+ String[] entries = connectionProperties.split(";");
+ Properties properties = new Properties();
+ for (String entry : entries) {
+ if (!entry.isEmpty()) {
+ int index = entry.indexOf('=');
+ if (index > 0) {
+ String name = entry.substring(0, index);
+ String value = entry.substring(index + 1);
+ properties.setProperty(name, value);
+ } else {
+ // no value is empty string which is how
+ // java.util.Properties works
+ properties.setProperty(entry, "");
+ }
+ }
+ }
+ return properties;
+ }
+
// used to hold per-thread singleton connection pools
- private static final ThreadLocal