diff --git a/src/protocol/bolt/src/main/java/org/apache/jmeter/protocol/bolt/config/BoltConnectionElement.java b/src/protocol/bolt/src/main/java/org/apache/jmeter/protocol/bolt/config/BoltConnectionElement.java
index c3211dc49f6..8b1a41f0634 100644
--- a/src/protocol/bolt/src/main/java/org/apache/jmeter/protocol/bolt/config/BoltConnectionElement.java
+++ b/src/protocol/bolt/src/main/java/org/apache/jmeter/protocol/bolt/config/BoltConnectionElement.java
@@ -26,11 +26,13 @@
import org.apache.jmeter.threads.JMeterContextService;
import org.apache.jmeter.threads.JMeterVariables;
import org.neo4j.driver.AuthTokens;
+import org.neo4j.driver.Config;
import org.neo4j.driver.Driver;
import org.neo4j.driver.GraphDatabase;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+
@TestElementMetadata(labelResource = "displayName")
public class BoltConnectionElement extends AbstractTestElement
implements ConfigElement, TestStateListener, TestBean {
@@ -39,6 +41,7 @@ public class BoltConnectionElement extends AbstractTestElement
private String boltUri;
private String username;
private String password;
+ private int maxConnectionPoolSize;
private Driver driver;
public static final String BOLT_CONNECTION = "boltConnection";
@@ -65,7 +68,10 @@ public void testStarted() {
log.error("Bolt connection already exists");
} else {
synchronized (this) {
- driver = GraphDatabase.driver(getBoltUri(), AuthTokens.basic(getUsername(), getPassword()));
+ Config config = Config.builder()
+ .withMaxConnectionPoolSize( getMaxConnectionPoolSize() )
+ .build();
+ driver = GraphDatabase.driver(getBoltUri(), AuthTokens.basic(getUsername(), getPassword()), config);
variables.putObject(BOLT_CONNECTION, driver);
}
}
@@ -100,6 +106,14 @@ public void setBoltUri(String boltUri) {
this.boltUri = boltUri;
}
+ public int getMaxConnectionPoolSize() {
+ return maxConnectionPoolSize;
+ }
+
+ public void setMaxConnectionPoolSize(int maxConnectionPoolSize) {
+ this.maxConnectionPoolSize = maxConnectionPoolSize;
+ }
+
public String getUsername() {
return username;
}
diff --git a/src/protocol/bolt/src/main/java/org/apache/jmeter/protocol/bolt/config/BoltConnectionElementBeanInfo.java b/src/protocol/bolt/src/main/java/org/apache/jmeter/protocol/bolt/config/BoltConnectionElementBeanInfo.java
index 9a8b75f2cd4..40711d4bdf0 100644
--- a/src/protocol/bolt/src/main/java/org/apache/jmeter/protocol/bolt/config/BoltConnectionElementBeanInfo.java
+++ b/src/protocol/bolt/src/main/java/org/apache/jmeter/protocol/bolt/config/BoltConnectionElementBeanInfo.java
@@ -33,7 +33,7 @@ public class BoltConnectionElementBeanInfo extends BeanInfoSupport {
public BoltConnectionElementBeanInfo() {
super(BoltConnectionElement.class);
- createPropertyGroup("connection", new String[] { "boltUri", "username", "password" });
+ createPropertyGroup("connection", new String[] { "boltUri", "username", "password", "maxConnectionPoolSize" });
PropertyDescriptor propertyDescriptor = property("boltUri");
propertyDescriptor.setValue(NOT_UNDEFINED, Boolean.TRUE);
@@ -44,6 +44,9 @@ public BoltConnectionElementBeanInfo() {
propertyDescriptor = property("password", TypeEditor.PasswordEditor);
propertyDescriptor.setValue(NOT_UNDEFINED, Boolean.TRUE);
propertyDescriptor.setValue(DEFAULT, "");
+ propertyDescriptor = property("maxConnectionPoolSize");
+ propertyDescriptor.setValue(NOT_UNDEFINED, Boolean.TRUE);
+ propertyDescriptor.setValue(DEFAULT, 100);
if(log.isDebugEnabled()) {
String descriptorsAsString = Arrays.stream(getPropertyDescriptors())
diff --git a/src/protocol/bolt/src/main/java/org/apache/jmeter/protocol/bolt/sampler/AbstractBoltTestElement.java b/src/protocol/bolt/src/main/java/org/apache/jmeter/protocol/bolt/sampler/AbstractBoltTestElement.java
index 5f77d46581c..5b996817f68 100644
--- a/src/protocol/bolt/src/main/java/org/apache/jmeter/protocol/bolt/sampler/AbstractBoltTestElement.java
+++ b/src/protocol/bolt/src/main/java/org/apache/jmeter/protocol/bolt/sampler/AbstractBoltTestElement.java
@@ -17,13 +17,52 @@
package org.apache.jmeter.protocol.bolt.sampler;
+import java.time.Duration;
+
+import org.apache.commons.lang3.EnumUtils;
+import org.apache.commons.lang3.StringUtils;
import org.apache.jmeter.testelement.AbstractTestElement;
+import org.neo4j.driver.AccessMode;
+import org.neo4j.driver.SessionConfig;
+import org.neo4j.driver.TransactionConfig;
public abstract class AbstractBoltTestElement extends AbstractTestElement {
private String cypher;
private String params;
+ private String database;
+ private String accessMode;
private boolean recordQueryResults;
+ private int txTimeout;
+
+ public int getTxTimeout() {
+ return txTimeout;
+ }
+
+ public void setTxTimeout(int txTimeout) {
+ this.txTimeout = txTimeout;
+ }
+
+ public String getAccessMode() {
+ if (accessMode == null) {
+ return AccessMode.WRITE.toString();
+ }
+ return accessMode;
+ }
+
+ public void setAccessMode(String accessMode) {
+ if (EnumUtils.isValidEnum(AccessMode.class, accessMode)) {
+ this.accessMode = accessMode;
+ }
+ }
+
+ public String getDatabase() {
+ return database;
+ }
+
+ public void setDatabase(String database) {
+ this.database = database;
+ }
public String getCypher() {
return cypher;
@@ -48,4 +87,27 @@ public boolean isRecordQueryResults() {
public void setRecordQueryResults(boolean recordQueryResults) {
this.recordQueryResults = recordQueryResults;
}
+
+ //returns a SessionConfig object that can be passed to the driver session
+ public SessionConfig getSessionConfig() {
+ SessionConfig.Builder sessionConfigBuilder = SessionConfig.builder()
+ .withDefaultAccessMode(Enum.valueOf(AccessMode.class, getAccessMode()));
+
+ if (StringUtils.isNotBlank(database)) {
+ sessionConfigBuilder.withDatabase(database);
+ }
+
+ return sessionConfigBuilder.build();
+ }
+
+ //returns a TransactionConfig object that can be passed to the driver transaction
+ public TransactionConfig getTransactionConfig() {
+ TransactionConfig.Builder txConfigBuilder = TransactionConfig.builder();
+
+ if (txTimeout > 0) {
+ txConfigBuilder.withTimeout(Duration.ofSeconds(txTimeout));
+ }
+
+ return txConfigBuilder.build();
+ }
}
diff --git a/src/protocol/bolt/src/main/java/org/apache/jmeter/protocol/bolt/sampler/BoltSampler.java b/src/protocol/bolt/src/main/java/org/apache/jmeter/protocol/bolt/sampler/BoltSampler.java
index 03b2c435379..94ec298e204 100644
--- a/src/protocol/bolt/src/main/java/org/apache/jmeter/protocol/bolt/sampler/BoltSampler.java
+++ b/src/protocol/bolt/src/main/java/org/apache/jmeter/protocol/bolt/sampler/BoltSampler.java
@@ -22,6 +22,7 @@
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -39,6 +40,8 @@
import org.neo4j.driver.Record;
import org.neo4j.driver.Result;
import org.neo4j.driver.Session;
+import org.neo4j.driver.SessionConfig;
+import org.neo4j.driver.TransactionConfig;
import org.neo4j.driver.exceptions.Neo4jException;
import org.neo4j.driver.summary.ResultSummary;
@@ -82,7 +85,14 @@ public SampleResult sample(Entry e) {
try {
res.setResponseHeaders("Cypher request: " + getCypher());
- res.setResponseData(execute(BoltConnectionElement.getDriver(), getCypher(), params), StandardCharsets.UTF_8.name());
+ res.setResponseData(
+ execute(
+ BoltConnectionElement.getDriver(),
+ getCypher(),
+ params,
+ getSessionConfig(),
+ getTransactionConfig()),
+ StandardCharsets.UTF_8.name());
} catch (Exception ex) {
res = handleException(res, ex);
} finally {
@@ -100,9 +110,10 @@ public boolean applies(ConfigTestElement configElement) {
return APPLICABLE_CONFIG_CLASSES.contains(guiClass);
}
- private String execute(Driver driver, String cypher, Map This sampler allows you to run Cypher queries through the Bolt protocol. Before using this you need to set up a Every request uses a connection acquired from the pool and returns it to the pool when the sampler completes.
- The connection pool size use the driver defaults (~100) and is not configurable at the moment.
for a direct connection: bolt://<HOST>:<PORT>
for a cluster: neo4j://<HOST>:<PORT>[?<ROUTING_CONTEXT>])
username.displayName=Username
username.shortDescription=Username
password.displayName=Password
password.shortDescription=Password
+maxConnectionPoolSize.displayName=Connection Pool Max Size
+maxConnectionPoolSize.shortDescription=Size limit for the pool of Bolt connections
diff --git a/src/protocol/bolt/src/main/resources/org/apache/jmeter/protocol/bolt/sampler/BoltSamplerResources.properties b/src/protocol/bolt/src/main/resources/org/apache/jmeter/protocol/bolt/sampler/BoltSamplerResources.properties
index 7b606b71743..fda9c0ababb 100644
--- a/src/protocol/bolt/src/main/resources/org/apache/jmeter/protocol/bolt/sampler/BoltSamplerResources.properties
+++ b/src/protocol/bolt/src/main/resources/org/apache/jmeter/protocol/bolt/sampler/BoltSamplerResources.properties
@@ -17,9 +17,16 @@
displayName=Bolt Request
query.displayName=Query
+options.displayName=Options
cypher.displayName=Cypher Statement
cypher.shortDescription=Cypher Statement
params.displayName=Params
params.shortDescription=Params
recordQueryResults.displayName=Record Query Results
recordQueryResults.shortDescription=Records the results of queries and displays in listeners such as View Results Tree, this iterates through the entire resultset. Use to debug only.
+accessMode.displayName=Access Mode
+accessMode.shortDescription=Whether it's a READ or WRITE query (affects query routing in clusters)
+database.displayName=Database
+database.shortDescription=Neo4j 4.x: database to query (leave empty for 3.5)
+txTimeout.displayName=Transaction timeout
+txTimeout.shortDescription=Transaction timeout in seconds
diff --git a/src/protocol/bolt/src/test/groovy/org/apache/jmeter/protocol/bolt/sampler/BoltSamplerSpec.groovy b/src/protocol/bolt/src/test/groovy/org/apache/jmeter/protocol/bolt/sampler/BoltSamplerSpec.groovy
index ffd3b5abf16..a6062c7949e 100644
--- a/src/protocol/bolt/src/test/groovy/org/apache/jmeter/protocol/bolt/sampler/BoltSamplerSpec.groovy
+++ b/src/protocol/bolt/src/test/groovy/org/apache/jmeter/protocol/bolt/sampler/BoltSamplerSpec.groovy
@@ -22,6 +22,7 @@ import org.apache.jmeter.samplers.Entry
import org.apache.jmeter.threads.JMeterContextService
import org.apache.jmeter.threads.JMeterVariables
import org.neo4j.driver.Driver
+import org.neo4j.driver.Record
import org.neo4j.driver.Result
import org.neo4j.driver.Session
import org.neo4j.driver.exceptions.ClientException
@@ -47,13 +48,13 @@ class BoltSamplerSpec extends Specification {
JMeterContextService.getContext().setVariables(variables)
entry.addConfigElement(boltConfig)
session = Mock(Session)
- driver.session() >> session
+ driver.session(_) >> session
}
def "should execute return success on successful query"() {
given:
sampler.setCypher("MATCH x")
- session.run("MATCH x", [:]) >> getEmptyQueryResult()
+ session.run("MATCH x", [:], _) >> getEmptyQueryResult()
when:
def response = sampler.sample(entry)
then:
@@ -67,10 +68,44 @@ class BoltSamplerSpec extends Specification {
response.getTime() > 0
}
+ def "should not display results by default"() {
+ given:
+ sampler.setCypher("MATCH x")
+ session.run("MATCH x", [:], _) >> getPopulatedQueryResult()
+ when:
+ def response = sampler.sample(entry)
+ then:
+ response.isSuccessful()
+ response.isResponseCodeOK()
+ def str = response.getResponseDataAsString()
+ str.contains("Summary:")
+ str.endsWith("Records: Skipped")
+ response.getSampleCount() == 1
+ response.getErrorCount() == 0
+ }
+
+ def "should display results if asked"() {
+ given:
+ sampler.setCypher("MATCH x")
+ sampler.setRecordQueryResults(true)
+ session.run("MATCH x", [:], _) >> getPopulatedQueryResult()
+ when:
+ def response = sampler.sample(entry)
+ then:
+ response.isSuccessful()
+ response.isResponseCodeOK()
+ def str = response.getResponseDataAsString()
+ str.contains("Summary:")
+ str.endsWith("Mock for type 'Record'")
+ response.getSampleCount() == 1
+ response.getErrorCount() == 0
+ response.getTime() > 0
+ }
+
def "should return error on failed query"() {
given:
sampler.setCypher("MATCH x")
- session.run("MATCH x", [:]) >> { throw new RuntimeException("a message") }
+ session.run("MATCH x", [:], _) >> { throw new RuntimeException("a message") }
when:
def response = sampler.sample(entry)
then:
@@ -104,13 +139,30 @@ class BoltSamplerSpec extends Specification {
def "should return db error code"() {
given:
sampler.setCypher("MATCH x")
- session.run("MATCH x", [:]) >> { throw new ClientException("a code", "a message") }
+ session.run("MATCH x", [:], _) >> { throw new ClientException("a code", "a message") }
when:
def response = sampler.sample(entry)
then:
response.getResponseCode() == "a code"
}
+ def "should ignore invalid timeout values"() {
+ given:
+ sampler.setCypher("MATCH x")
+ sampler.setTxTimeout(-1)
+ session.run("MATCH x", [:], _) >> getEmptyQueryResult()
+ when:
+ def response = sampler.sample(entry)
+ then:
+ response.isSuccessful()
+ response.isResponseCodeOK()
+ def str = response.getResponseDataAsString()
+ str.contains("Summary:")
+ str.endsWith("Records: Skipped")
+ response.getSampleCount() == 1
+ response.getErrorCount() == 0
+ }
+
def getEmptyQueryResult() {
def queryResult = Mock(Result)
def summary = Mock(ResultSummary)
@@ -119,4 +171,15 @@ class BoltSamplerSpec extends Specification {
summary.counters() >> counters
return queryResult
}
+
+ def getPopulatedQueryResult() {
+ def queryResult = Mock(Result)
+ def summary = Mock(ResultSummary)
+ def list = [Mock(Record), Mock(Record), Mock(Record)]
+ queryResult.consume() >> summary
+ queryResult.list() >> list
+ SummaryCounters counters = Mock(SummaryCounters)
+ summary.counters() >> counters
+ return queryResult
+ }
}
diff --git a/xdocs/changes.xml b/xdocs/changes.xml
index 140c0f4c887..50cfff1084c 100644
--- a/xdocs/changes.xml
+++ b/xdocs/changes.xml
@@ -82,6 +82,9 @@ Summary
Other samplers
+
Controllers
@@ -96,6 +99,7 @@ Summary
Timers, Assertions, Config, Pre- & Post-Processors
+
Functions
@@ -128,6 +132,10 @@ Summary
Other Samplers
+
SocketTimeoutException on BinaryTCPClientImpl, when no EOM Byte is set. Regression
+ introduced by commit c190641e4f0474a34a366a72364b0a8dd25bfc81 which fixed Controllers
diff --git a/xdocs/images/screenshots/bolt-connection-config.png b/xdocs/images/screenshots/bolt-connection-config.png
index 3afe34ac3e6..eddecc359ae 100644
Binary files a/xdocs/images/screenshots/bolt-connection-config.png and b/xdocs/images/screenshots/bolt-connection-config.png differ
diff --git a/xdocs/images/screenshots/bolt-request.png b/xdocs/images/screenshots/bolt-request.png
index 521772ec597..ce04152806c 100644
Binary files a/xdocs/images/screenshots/bolt-request.png and b/xdocs/images/screenshots/bolt-request.png differ
diff --git a/xdocs/usermanual/component_reference.xml b/xdocs/usermanual/component_reference.xml
index 860d32e196b..08f9d6267ab 100644
--- a/xdocs/usermanual/component_reference.xml
+++ b/xdocs/usermanual/component_reference.xml
@@ -1968,7 +1968,7 @@ MongoDB Script is more suitable for functional testing or test setup (setup/tear
The measured response time corresponds to the "full" query execution, including both the time to execute the cypher query AND the time to consume the results sent back by the database.
@@ -1984,6 +1984,13 @@ MongoDB Script is more suitable for functional testing or test setup (setup/tear Whether to add or not query result data to the sampler response (default false). Note that activating this has a memory overhead, use it wisely. +