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 params) { - try (Session session = driver.session()) { - Result statementResult = session.run(cypher, params); + private String execute(Driver driver, String cypher, Map params, + SessionConfig sessionConfig, TransactionConfig txConfig) { + try (Session session = driver.session(sessionConfig)) { + Result statementResult = session.run(cypher, params, txConfig); return response(statementResult); } } @@ -135,12 +146,25 @@ private String request() { .append(getCypher()) .append("\n") .append("Parameters: \n") - .append(getParams()); + .append(getParams()) + .append("\n") + .append("Database: \n") + .append(getDatabase()) + .append("\n") + .append("Access Mode: \n") + .append(getAccessMode()); return request.toString(); } private String response(Result result) { StringBuilder response = new StringBuilder(); + List records; + if (isRecordQueryResults()) { + //get records already as consume() will exhaust the stream + records = result.list(); + } else { + records = Collections.emptyList(); + } response.append("\nSummary:"); ResultSummary summary = result.consume(); response.append("\nConstraints Added: ") @@ -167,7 +191,7 @@ private String response(Result result) { .append(summary.counters().relationshipsDeleted()); response.append("\n\nRecords: "); if (isRecordQueryResults()) { - for (Record record : result.list()) { + for (Record record : records) { response.append("\n").append(record); } } else { diff --git a/src/protocol/bolt/src/main/java/org/apache/jmeter/protocol/bolt/sampler/BoltTestElementBeanInfoSupport.java b/src/protocol/bolt/src/main/java/org/apache/jmeter/protocol/bolt/sampler/BoltTestElementBeanInfoSupport.java index 0be4e9e3506..a4c93a5cfd4 100644 --- a/src/protocol/bolt/src/main/java/org/apache/jmeter/protocol/bolt/sampler/BoltTestElementBeanInfoSupport.java +++ b/src/protocol/bolt/src/main/java/org/apache/jmeter/protocol/bolt/sampler/BoltTestElementBeanInfoSupport.java @@ -18,10 +18,12 @@ package org.apache.jmeter.protocol.bolt.sampler; import java.beans.PropertyDescriptor; +import java.util.Arrays; import org.apache.jmeter.testbeans.BeanInfoSupport; import org.apache.jmeter.testbeans.TestBean; import org.apache.jmeter.testbeans.gui.TypeEditor; +import org.neo4j.driver.AccessMode; public abstract class BoltTestElementBeanInfoSupport extends BeanInfoSupport { /** @@ -33,17 +35,35 @@ protected BoltTestElementBeanInfoSupport(Class beanClass) { super(beanClass); createPropertyGroup("query", new String[] { "cypher","params","recordQueryResults"}); + createPropertyGroup("options", new String[] { "accessMode","database", "txTimeout"}); - PropertyDescriptor propertyDescriptor = property("cypher", TypeEditor.TextAreaEditor); + PropertyDescriptor propertyDescriptor = property("cypher", TypeEditor.TextAreaEditor); propertyDescriptor.setValue(NOT_UNDEFINED, Boolean.TRUE); propertyDescriptor.setValue(DEFAULT, ""); - propertyDescriptor = property("params", TypeEditor.TextAreaEditor); + propertyDescriptor = property("params", TypeEditor.TextAreaEditor); propertyDescriptor.setValue(NOT_UNDEFINED, Boolean.TRUE); propertyDescriptor.setValue(DEFAULT, "{\"paramName\":\"paramValue\"}"); - propertyDescriptor = property("recordQueryResults"); + propertyDescriptor = property("recordQueryResults"); propertyDescriptor.setValue(NOT_UNDEFINED, Boolean.TRUE); propertyDescriptor.setValue(DEFAULT, Boolean.FALSE); + + propertyDescriptor = property("accessMode", TypeEditor.ComboStringEditor); + propertyDescriptor.setValue(NOT_UNDEFINED, Boolean.TRUE); + propertyDescriptor.setValue(NOT_EXPRESSION, Boolean.TRUE); + propertyDescriptor.setValue(DEFAULT, AccessMode.WRITE.toString()); + propertyDescriptor.setValue(TAGS, getListAccessModes()); + + propertyDescriptor = property("database", TypeEditor.ComboStringEditor); + propertyDescriptor.setValue(DEFAULT, "neo4j"); + + propertyDescriptor = property("txTimeout"); + propertyDescriptor.setValue(NOT_UNDEFINED, Boolean.TRUE); + propertyDescriptor.setValue(DEFAULT, 60); + } + + private String[] getListAccessModes() { + return Arrays.stream(AccessMode.values()).map(Enum::toString).toArray(String[]::new); } } diff --git a/src/protocol/bolt/src/main/resources/org/apache/jmeter/protocol/bolt/config/BoltConnectionElementResources.properties b/src/protocol/bolt/src/main/resources/org/apache/jmeter/protocol/bolt/config/BoltConnectionElementResources.properties index 98d2cc6a419..85d922dac9e 100644 --- a/src/protocol/bolt/src/main/resources/org/apache/jmeter/protocol/bolt/config/BoltConnectionElementResources.properties +++ b/src/protocol/bolt/src/main/resources/org/apache/jmeter/protocol/bolt/config/BoltConnectionElementResources.properties @@ -18,8 +18,10 @@ displayName=Bolt Connection Configuration connection.displayName=Bolt Configuration boltUri.displayName=Bolt URI -boltUri.shortDescription=Bolt URI +boltUri.shortDescription=Bolt URI
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

    +
  • Bolt Sampler : added "transaction timeout" option
  • +
  • Bolt Sampler : added "database" option, required for Neo4j 4.x (with multi-database support)
  • +
  • Bolt Sampler : added "access mode" option, that allows running against a Neo4j Enterprise causal cluster

Controllers

@@ -96,6 +99,7 @@ Summary

Timers, Assertions, Config, Pre- & Post-Processors

    +
  • Bolt Connection Configuration: added ConnectionPoolMaxSize parameter

Functions

@@ -128,6 +132,10 @@ Summary

Other Samplers

    +
  • Bolt Sampler : fixed error displaying results when "Record Query Results" is enabled.
  • +
  • 65034Ignore SocketTimeoutException on BinaryTCPClientImpl, when no EOM Byte is set. Regression + introduced by commit c190641e4f0474a34a366a72364b0a8dd25bfc81 which fixed 52104. That bug was bout handling + the case of waiting for an EOM.

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

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.

+ The connection pool size defaults to 100 and is configurable.

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. + Whether to access the database in WRITE or READ mode. + Use WRITE for a standalone Neo4j instance. + For a Neo4j cluster, select mode depending on whether the query writes to the database. + That setting will allow correct routing to the cluster leader, followers or read replicas. + The database to run the query against. + Required for Neo4j 4.0+, unless querying the default database. Must be undefined for Neo4j 3.5. + Timeout for the transaction. It is strongly advised to use query parameters, allowing the database to cache and reuse execution plans. @@ -4522,6 +4529,8 @@ DB db = MongoDBHolder.getDBFromSource("value of property MongoDB Source", The database URI. User account. User credentials. + Max size of the Neo4j driver Bolt connection pool. + Raise the value if running large number of concurrent threads, so that JMeter threads are not blocked waiting for a connection to be released to the pool.