You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jmeter.apache.org by pm...@apache.org on 2019/10/04 05:32:36 UTC

[jmeter] branch master updated: Add Bolt protocol support for Neo4j database (#510)

This is an automated email from the ASF dual-hosted git repository.

pmouawad pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/jmeter.git


The following commit(s) were added to refs/heads/master by this push:
     new 2cfdec9  Add Bolt protocol support for Neo4j database (#510)
2cfdec9 is described below

commit 2cfdec9d50ddab4649b813c07724588a03371ef4
Author: Nicolas Mervaillie <nm...@users.noreply.github.com>
AuthorDate: Fri Oct 4 07:32:30 2019 +0200

    Add Bolt protocol support for Neo4j database (#510)
    
    * Add Bolt protocol support
    
    Add driver dependency
    Add bolt config element and sampler
    Expose cypher query parameter, execute cypher
    Add temporary README on how to build for hackathon submission
    
    * Add default example value for parameters to better guide users
    
    * Rename configuration classes for clarity
    
    * Apply remarks from code review and code cleanup
    
    * Use TextArea input for query and param fields
    
    Simple text inputs are too small
    
    * Add bolt elements to SaveService
    
    * Add documentation for bolt protocol
    
    * Fix build scripts
    
    Fix some dependencies and add the bolt protocol to the dist build
    
    * Remove README-bolt.md to submit PR
    
    * Fix failing test
    
    build says: SaveService nameMap (saveservice.properties) should contain org.apache.jmeter.protocol.bolt.sampler.AbstractBoltTestElement
    
    * Add some unit tests
    
    * Use single line logging instead of multi line
    
    * Avoid using lambda when consuming results to avoid performance hit
    
    * Add documentation about connection pooling and what's included in response time
    
    * Add neo4j driver trust-key
    
    
    This resolves Bug 63801 - Add Bolt protocol support for Neo4j database
    https://bz.apache.org/bugzilla/show_bug.cgi?id=63801
---
 bin/saveservice.properties                         |   2 +
 checksum.xml                                       |   1 +
 gradle.properties                                  |   1 +
 settings.gradle.kts                                |   1 +
 src/bom/build.gradle.kts                           |   1 +
 .../java/org/apache/jmeter/save/SaveService.java   |   2 +-
 src/dist/build.gradle.kts                          |   1 +
 .../bolt/config/BoltConnectionElement.java         | 121 ++++++++++++++
 .../bolt/config/BoltConnectionElementBeanInfo.java |  57 +++++++
 .../bolt/sampler/AbstractBoltTestElement.java      |  52 ++++++
 .../jmeter/protocol/bolt/sampler/BoltSampler.java  | 175 +++++++++++++++++++++
 .../protocol/bolt/sampler/BoltSamplerBeanInfo.java |  26 +++
 .../sampler/BoltTestElementBeanInfoSupport.java    |  50 ++++++
 .../BoltConnectionElementResources.properties      |  26 +++
 .../bolt/sampler/BoltSamplerResources.properties   |  28 ++++
 .../protocol/bolt/sampler/BoltSamplerSpec.groovy   | 123 +++++++++++++++
 .../jmeter/resources/ResourceKeyUsageTestBolt.java |  23 +++
 src/protocol/build.gradle.kts                      |   9 ++
 .../images/screenshots/bolt-connection-config.png  | Bin 0 -> 46364 bytes
 xdocs/images/screenshots/bolt-request.png          | Bin 0 -> 139114 bytes
 xdocs/usermanual/component_reference.xml           |  49 ++++++
 21 files changed, 747 insertions(+), 1 deletion(-)

diff --git a/bin/saveservice.properties b/bin/saveservice.properties
index e271e26..4f861db 100644
--- a/bin/saveservice.properties
+++ b/bin/saveservice.properties
@@ -247,6 +247,8 @@ MongoSourceElement=org.apache.jmeter.protocol.mongodb.config.MongoSourceElement
 MonitorHealthVisualizer=org.apache.jmeter.visualizers.MonitorHealthVisualizer
 
 NamePanel=org.apache.jmeter.gui.NamePanel
+BoltSampler=org.apache.jmeter.protocol.bolt.sampler.BoltSampler
+BoltConnectionElement=org.apache.jmeter.protocol.bolt.config.BoltConnectionElement
 ObsoleteGui=org.apache.jmeter.config.gui.ObsoleteGui
 OnceOnlyController=org.apache.jmeter.control.OnceOnlyController
 OnceOnlyControllerGui=org.apache.jmeter.control.gui.OnceOnlyControllerGui
diff --git a/checksum.xml b/checksum.xml
index 53d9eae..65ca9a2 100644
--- a/checksum.xml
+++ b/checksum.xml
@@ -119,6 +119,7 @@
     <trusted-key id='85911f425ec61b51' group='org.junit.vintage' />
     <trusted-key id='82216a03caa86c78' group='org.mongodb' />
     <trusted-key id='3f36885c24df4b75' group='org.mozilla' />
+    <trusted-key id='7ad289796be2ffe2' group='org.neo4j.driver' />
     <trusted-key id='7c7d8456294423ba' group='org.objenesis' />
     <trusted-key id='85911f425ec61b51' group='org.opentest4j' />
     <trusted-key id='5f69ad087600b22c' group='org.ow2.asm' />
diff --git a/gradle.properties b/gradle.properties
index a94e5f3..2107116 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -85,6 +85,7 @@ log4j.version=2.12.1
 mail.version=1.5.0-b01
 mina-core.version=2.0.19
 mongo-java-driver.version=2.11.3
+neo4j-java-driver.version=1.7.5
 objenesis.version=2.6
 oro.version=2.0.8
 ph-commons.version=9.3.7
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 2c78a23..9b65857 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -32,6 +32,7 @@ include(
         "src:generator",
         "src:jorphan",
         "src:licenses",
+        "src:protocol:bolt",
         "src:protocol:ftp",
         "src:protocol:http",
         "src:protocol:java",
diff --git a/src/bom/build.gradle.kts b/src/bom/build.gradle.kts
index 4a47ae6..cdcec48 100644
--- a/src/bom/build.gradle.kts
+++ b/src/bom/build.gradle.kts
@@ -136,6 +136,7 @@ dependencies {
         apiv("org.jsoup:jsoup")
         apiv("org.mongodb:mongo-java-driver")
         apiv("org.mozilla:rhino")
+        apiv("org.neo4j.driver:neo4j-java-driver")
         apiv("org.objenesis:objenesis")
         apiv("org.slf4j:jcl-over-slf4j", "slf4j")
         apiv("org.slf4j:slf4j-api", "slf4j")
diff --git a/src/core/src/main/java/org/apache/jmeter/save/SaveService.java b/src/core/src/main/java/org/apache/jmeter/save/SaveService.java
index efd3e99..25d6354 100644
--- a/src/core/src/main/java/org/apache/jmeter/save/SaveService.java
+++ b/src/core/src/main/java/org/apache/jmeter/save/SaveService.java
@@ -156,7 +156,7 @@ public class SaveService {
     private static String fileVersion = ""; // computed from saveservice.properties file// $NON-NLS-1$
     // Must match the sha1 checksum of the file saveservice.properties (without newline character),
     // used to ensure saveservice.properties and SaveService are updated simultaneously
-    static final String FILEVERSION = "1912808b50358c3afce8c54280f173b8fa8ba229"; // Expected value $NON-NLS-1$
+    static final String FILEVERSION = "6fd03656cf4997fe6b0af17fa8dc8469e563c93a"; // Expected value $NON-NLS-1$
 
     private static String fileEncoding = ""; // read from properties file// $NON-NLS-1$
 
diff --git a/src/dist/build.gradle.kts b/src/dist/build.gradle.kts
index 597bded..cb1e6a3 100644
--- a/src/dist/build.gradle.kts
+++ b/src/dist/build.gradle.kts
@@ -37,6 +37,7 @@ var jars = arrayOf(
         // ":src:examples",
         ":src:functions",
         ":src:jorphan",
+        ":src:protocol:bolt",
         ":src:protocol:ftp",
         ":src:protocol:http",
         ":src:protocol:java",
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
new file mode 100644
index 0000000..a92a787
--- /dev/null
+++ b/src/protocol/bolt/src/main/java/org/apache/jmeter/protocol/bolt/config/BoltConnectionElement.java
@@ -0,0 +1,121 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.jmeter.protocol.bolt.config;
+
+import org.apache.jmeter.config.ConfigElement;
+import org.apache.jmeter.testbeans.TestBean;
+import org.apache.jmeter.testbeans.TestBeanHelper;
+import org.apache.jmeter.testelement.AbstractTestElement;
+import org.apache.jmeter.testelement.TestStateListener;
+import org.apache.jmeter.threads.JMeterContextService;
+import org.apache.jmeter.threads.JMeterVariables;
+import org.neo4j.driver.v1.AuthTokens;
+import org.neo4j.driver.v1.Driver;
+import org.neo4j.driver.v1.GraphDatabase;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class BoltConnectionElement extends AbstractTestElement
+        implements ConfigElement, TestStateListener, TestBean {
+
+    private static final Logger log = LoggerFactory.getLogger(BoltConnectionElement.class);
+    private String boltUri;
+    private String username;
+    private String password;
+    private Driver driver;
+
+    public static final String BOLT_CONNECTION = "boltConnection";
+
+    public BoltConnectionElement() {
+    }
+
+    @Override
+    public void addConfigElement(ConfigElement config) {
+
+    }
+
+    @Override
+    public boolean expectsModification() {
+        return false;
+    }
+
+    @Override
+    public void testStarted() {
+        this.setRunningVersion(true);
+        TestBeanHelper.prepare(this);
+        JMeterVariables variables = getThreadContext().getVariables();
+        if (variables.getObject(BOLT_CONNECTION) != null) {
+            log.error("Bolt connection already exists");
+        } else {
+            synchronized (this) {
+                driver = GraphDatabase.driver(getBoltUri(), AuthTokens.basic(getUsername(), getPassword()));
+                variables.putObject(BOLT_CONNECTION, driver);
+            }
+        }
+    }
+
+    @Override
+    public void testStarted(String host) {
+        testStarted();
+    }
+
+    @Override
+    public void testEnded() {
+        synchronized (this) {
+            if (driver != null) {
+                driver.close();
+                driver = null;
+            }
+        }
+
+    }
+
+    @Override
+    public void testEnded(String host) {
+        testEnded();
+    }
+
+    public String getBoltUri() {
+        return boltUri;
+    }
+
+    public void setBoltUri(String boltUri) {
+        this.boltUri = boltUri;
+    }
+
+    public String getUsername() {
+        return username;
+    }
+
+    public void setUsername(String username) {
+        this.username = username;
+    }
+
+    public String getPassword() {
+        return password;
+    }
+
+    public void setPassword(String password) {
+        this.password = password;
+    }
+
+    public static Driver getDriver() {
+        return (Driver) JMeterContextService.getContext().getVariables().getObject(BOLT_CONNECTION);
+    }
+}
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
new file mode 100644
index 0000000..40b1543
--- /dev/null
+++ b/src/protocol/bolt/src/main/java/org/apache/jmeter/protocol/bolt/config/BoltConnectionElementBeanInfo.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.jmeter.protocol.bolt.config;
+
+import java.beans.PropertyDescriptor;
+import java.util.Arrays;
+import java.util.stream.Collectors;
+
+import org.apache.jmeter.testbeans.BeanInfoSupport;
+import org.apache.jmeter.testbeans.gui.TypeEditor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class BoltConnectionElementBeanInfo extends BeanInfoSupport {
+
+    private static final Logger log = LoggerFactory.getLogger(BoltConnectionElementBeanInfo.class);
+
+    public BoltConnectionElementBeanInfo() {
+        super(BoltConnectionElement.class);
+
+        createPropertyGroup("connection", new String[] { "boltUri", "username", "password" });
+
+        PropertyDescriptor propertyDescriptor =  property("boltUri");
+        propertyDescriptor.setValue(NOT_UNDEFINED, Boolean.TRUE);
+        propertyDescriptor.setValue(DEFAULT, "bolt://localhost:7687");
+        propertyDescriptor = property("username");
+        propertyDescriptor.setValue(NOT_UNDEFINED, Boolean.TRUE);
+        propertyDescriptor.setValue(DEFAULT, "neo4j");
+        propertyDescriptor = property("password", TypeEditor.PasswordEditor);
+        propertyDescriptor.setValue(NOT_UNDEFINED, Boolean.TRUE);
+        propertyDescriptor.setValue(DEFAULT, "");
+
+        if(log.isDebugEnabled()) {
+            String descriptorsAsString = Arrays.stream(getPropertyDescriptors())
+                    .map(pd -> pd.getName() + "=" + pd.getDisplayName())
+                    .collect(Collectors.joining(" ,"));
+            log.debug(descriptorsAsString);
+        }
+
+    }
+}
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
new file mode 100644
index 0000000..89594aa
--- /dev/null
+++ b/src/protocol/bolt/src/main/java/org/apache/jmeter/protocol/bolt/sampler/AbstractBoltTestElement.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.jmeter.protocol.bolt.sampler;
+
+import org.apache.jmeter.testelement.AbstractTestElement;
+
+public abstract class AbstractBoltTestElement extends AbstractTestElement {
+
+    private String cypher;
+    private String params;
+    private boolean recordQueryResults;
+
+    public String getCypher() {
+        return cypher;
+    }
+
+    public void setCypher(String cypher) {
+        this.cypher = cypher;
+    }
+
+    public String getParams() {
+        return params;
+    }
+
+    public void setParams(String params) {
+        this.params = params;
+    }
+
+    public boolean isRecordQueryResults() {
+        return recordQueryResults;
+    }
+
+    public void setRecordQueryResults(boolean recordQueryResults) {
+        this.recordQueryResults = recordQueryResults;
+    }
+}
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
new file mode 100644
index 0000000..dd24325
--- /dev/null
+++ b/src/protocol/bolt/src/main/java/org/apache/jmeter/protocol/bolt/sampler/BoltSampler.java
@@ -0,0 +1,175 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.jmeter.protocol.bolt.sampler;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.commons.lang3.ObjectUtils;
+import org.apache.jmeter.config.ConfigTestElement;
+import org.apache.jmeter.engine.util.ConfigMergabilityIndicator;
+import org.apache.jmeter.protocol.bolt.config.BoltConnectionElement;
+import org.apache.jmeter.samplers.Entry;
+import org.apache.jmeter.samplers.SampleResult;
+import org.apache.jmeter.samplers.Sampler;
+import org.apache.jmeter.testbeans.TestBean;
+import org.apache.jmeter.testelement.TestElement;
+import org.neo4j.driver.v1.Driver;
+import org.neo4j.driver.v1.Record;
+import org.neo4j.driver.v1.Session;
+import org.neo4j.driver.v1.StatementResult;
+import org.neo4j.driver.v1.exceptions.Neo4jException;
+import org.neo4j.driver.v1.summary.ResultSummary;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectReader;
+
+public class BoltSampler extends AbstractBoltTestElement implements Sampler, TestBean, ConfigMergabilityIndicator {
+
+    private static final Set<String> APPLICABLE_CONFIG_CLASSES = new HashSet<>(
+            Collections.singletonList("org.apache.jmeter.config.gui.SimpleConfigGui")); // $NON-NLS-1$
+
+    private static final ObjectReader objectMapper = new ObjectMapper().readerFor(new TypeReference<HashMap<String, Object>>() {});
+
+    @Override
+    public SampleResult sample(Entry e) {
+        SampleResult res = new SampleResult();
+        res.setSampleLabel(getName());
+        res.setSamplerData(request());
+        res.setDataType(SampleResult.TEXT);
+        res.setContentType("text/plain"); // $NON-NLS-1$
+        res.setDataEncoding(StandardCharsets.UTF_8.name());
+
+        Map<String, Object> params;
+        try {
+            params = getParamsAsMap();
+        } catch (IOException ex) {
+            return handleException(res, ex);
+        }
+
+        // Assume we will be successful
+        res.setSuccessful(true);
+        res.setResponseMessageOK();
+        res.setResponseCodeOK();
+
+        res.sampleStart();
+
+        try {
+            res.setResponseHeaders("Cypher request: " + getCypher());
+            res.setResponseData(execute(BoltConnectionElement.getDriver(), getCypher(), params), StandardCharsets.UTF_8.name());
+        } catch (Exception ex) {
+            res = handleException(res, ex);
+        } finally {
+            res.sampleEnd();
+        }
+        return res;
+    }
+
+    /**
+     * @see org.apache.jmeter.samplers.AbstractSampler#applies(org.apache.jmeter.config.ConfigTestElement)
+     */
+    @Override
+    public boolean applies(ConfigTestElement configElement) {
+        String guiClass = configElement.getProperty(TestElement.GUI_CLASS).getStringValue();
+        return APPLICABLE_CONFIG_CLASSES.contains(guiClass);
+    }
+
+    private String execute(Driver driver, String cypher, Map<String, Object> params) {
+        try (Session session = driver.session()) {
+            StatementResult statementResult = session.run(cypher, params);
+            return response(statementResult);
+        }
+    }
+
+    private SampleResult handleException(SampleResult res, Exception ex) {
+        res.setResponseMessage(ex.toString());
+        if (ex instanceof Neo4jException) {
+            res.setResponseCode(((Neo4jException)ex).code());
+        } else {
+            res.setResponseCode("500");
+        }
+        res.setResponseData(ObjectUtils.defaultIfNull(ex.getMessage(), "NO MESSAGE").getBytes());
+        res.setSuccessful(false);
+        return res;
+    }
+
+    private Map<String, Object> getParamsAsMap() throws IOException {
+        if (getParams() != null && getParams().length() > 0) {
+            return objectMapper.readValue(getParams());
+        } else {
+            return Collections.emptyMap();
+        }
+    }
+
+    private String request() {
+        StringBuilder request = new StringBuilder();
+        request.append("Query: \n")
+                .append(getCypher())
+                .append("\n")
+                .append("Parameters: \n")
+                .append(getParams());
+        return request.toString();
+    }
+
+    private String response(StatementResult result) {
+        StringBuilder response = new StringBuilder();
+        response.append("\nSummary:");
+        ResultSummary summary = result.summary();
+        response.append("\nConstraints Added: ")
+                .append(summary.counters().constraintsAdded())
+                .append("\nConstraints Removed: ")
+                .append(summary.counters().constraintsRemoved())
+                .append("\nContains Updates: ")
+                .append(summary.counters().containsUpdates())
+                .append("\nIndexes Added: ")
+                .append(summary.counters().indexesAdded())
+                .append("\nIndexes Removed: ")
+                .append(summary.counters().indexesRemoved())
+                .append("\nLabels Added: ")
+                .append(summary.counters().labelsAdded())
+                .append("\nLabels Removed: ")
+                .append(summary.counters().labelsRemoved())
+                .append("\nNodes Created: ")
+                .append(summary.counters().nodesCreated())
+                .append("\nNodes Deleted: ")
+                .append(summary.counters().nodesDeleted())
+                .append("\nRelationships Created: ")
+                .append(summary.counters().relationshipsCreated())
+                .append("\nRelationships Deleted: ")
+                .append(summary.counters().relationshipsDeleted());
+        response.append("\n\nRecords: ");
+        if (isRecordQueryResults()) {
+            for (Record record : result.list()) {
+                response.append("\n").append(record);
+            }
+        } else {
+            response.append("Skipped");
+            result.consume();
+        }
+
+
+        return response.toString();
+    }
+}
diff --git a/src/protocol/bolt/src/main/java/org/apache/jmeter/protocol/bolt/sampler/BoltSamplerBeanInfo.java b/src/protocol/bolt/src/main/java/org/apache/jmeter/protocol/bolt/sampler/BoltSamplerBeanInfo.java
new file mode 100644
index 0000000..5e6c114
--- /dev/null
+++ b/src/protocol/bolt/src/main/java/org/apache/jmeter/protocol/bolt/sampler/BoltSamplerBeanInfo.java
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.jmeter.protocol.bolt.sampler;
+
+public class BoltSamplerBeanInfo extends BoltTestElementBeanInfoSupport {
+
+    public BoltSamplerBeanInfo() {
+        super(BoltSampler.class);
+    }
+}
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
new file mode 100644
index 0000000..5839b63
--- /dev/null
+++ b/src/protocol/bolt/src/main/java/org/apache/jmeter/protocol/bolt/sampler/BoltTestElementBeanInfoSupport.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.jmeter.protocol.bolt.sampler;
+
+import java.beans.PropertyDescriptor;
+
+import org.apache.jmeter.testbeans.BeanInfoSupport;
+import org.apache.jmeter.testbeans.TestBean;
+import org.apache.jmeter.testbeans.gui.TypeEditor;
+
+public abstract class BoltTestElementBeanInfoSupport extends BeanInfoSupport {
+    /**
+     * Construct a BeanInfo for the given class.
+     *
+     * @param beanClass class for which to construct a BeanInfo
+     */
+    protected BoltTestElementBeanInfoSupport(Class<? extends TestBean> beanClass) {
+        super(beanClass);
+
+        createPropertyGroup("query", new String[] { "cypher","params","recordQueryResults"});
+
+        PropertyDescriptor propertyDescriptor =  property("cypher", TypeEditor.TextAreaEditor);
+        propertyDescriptor.setValue(NOT_UNDEFINED, Boolean.TRUE);
+        propertyDescriptor.setValue(DEFAULT, "");
+
+        propertyDescriptor =  property("params", TypeEditor.TextAreaEditor);
+        propertyDescriptor.setValue(NOT_UNDEFINED, Boolean.TRUE);
+        propertyDescriptor.setValue(DEFAULT, "{\"paramName\":\"paramValue\"}");
+
+        propertyDescriptor =  property("recordQueryResults");
+        propertyDescriptor.setValue(NOT_UNDEFINED, Boolean.TRUE);
+        propertyDescriptor.setValue(DEFAULT, Boolean.FALSE);
+    }
+}
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
new file mode 100644
index 0000000..b3050ef
--- /dev/null
+++ b/src/protocol/bolt/src/main/resources/org/apache/jmeter/protocol/bolt/config/BoltConnectionElementResources.properties
@@ -0,0 +1,26 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+#
+#
+
+displayName=Bolt Connection Configuration
+connection.displayName=Bolt Configuration
+boltUri.displayName=Bolt URI
+boltUri.shortDescription=Bolt URI
+username.displayName=Username
+username.shortDescription=Username
+password.displayName=Password
+password.shortDescription=Password
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
new file mode 100644
index 0000000..c14ff03
--- /dev/null
+++ b/src/protocol/bolt/src/main/resources/org/apache/jmeter/protocol/bolt/sampler/BoltSamplerResources.properties
@@ -0,0 +1,28 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+#
+#
+
+displayName=Bolt Request
+query.displayName=Query
+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.
+
+
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
new file mode 100644
index 0000000..cd43bc6
--- /dev/null
+++ b/src/protocol/bolt/src/test/groovy/org/apache/jmeter/protocol/bolt/sampler/BoltSamplerSpec.groovy
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.jmeter.protocol.bolt.sampler
+
+import org.apache.jmeter.protocol.bolt.config.BoltConnectionElement
+import org.apache.jmeter.samplers.Entry
+import org.apache.jmeter.threads.JMeterContextService
+import org.apache.jmeter.threads.JMeterVariables
+import org.neo4j.driver.v1.Driver
+import org.neo4j.driver.v1.Session
+import org.neo4j.driver.v1.StatementResult
+import org.neo4j.driver.v1.exceptions.ClientException
+import org.neo4j.driver.v1.summary.ResultSummary
+import org.neo4j.driver.v1.summary.SummaryCounters
+
+import spock.lang.Specification
+
+class BoltSamplerSpec extends Specification {
+
+    BoltSampler sampler
+    Entry entry
+    Session session
+
+    def setup() {
+        sampler = new BoltSampler()
+        entry = new Entry()
+        def driver = Mock(Driver)
+        def boltConfig = new BoltConnectionElement()
+        def variables = new JMeterVariables()
+        // ugly but could not find a better way to pass the driver to the sampler...
+        variables.putObject(BoltConnectionElement.BOLT_CONNECTION, driver)
+        JMeterContextService.getContext().setVariables(variables)
+        entry.addConfigElement(boltConfig)
+        session = Mock(Session)
+        driver.session() >> session
+    }
+
+    def "should execute return success on successful query"() {
+        given:
+            sampler.setCypher("MATCH x")
+            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
+            response.getTime() > 0
+    }
+
+    def "should return error on failed query"() {
+        given:
+            sampler.setCypher("MATCH x")
+            session.run("MATCH x", [:]) >> { throw new RuntimeException("a message") }
+        when:
+            def response = sampler.sample(entry)
+        then:
+            !response.isSuccessful()
+            !response.isResponseCodeOK()
+            response.getResponseCode() == "500"
+            def str = response.getResponseDataAsString()
+            str.contains("a message")
+            response.getSampleCount() == 1
+            response.getErrorCount() == 1
+            response.getTime() > 0
+    }
+
+    def "should return error on invalid parameters"() {
+        given:
+            sampler.setCypher("MATCH x")
+            sampler.setParams("{invalid}")
+        when:
+            def response = sampler.sample(entry)
+        then:
+            !response.isSuccessful()
+            !response.isResponseCodeOK()
+            response.getResponseCode() == "500"
+            def str = response.getResponseDataAsString()
+            str.contains("Unexpected character")
+            response.getSampleCount() == 1
+            response.getErrorCount() == 1
+            response.getTime() == 0
+    }
+
+    def "should return db error code"() {
+        given:
+            sampler.setCypher("MATCH x")
+            session.run("MATCH x", [:]) >> { throw new ClientException("a code", "a message") }
+        when:
+            def response = sampler.sample(entry)
+        then:
+            response.getResponseCode() == "a code"
+    }
+
+    def getEmptyQueryResult() {
+        def queryResult = Mock(StatementResult)
+        def summary = Mock(ResultSummary)
+        queryResult.summary() >> summary
+        SummaryCounters counters = Mock(SummaryCounters)
+        summary.counters() >> counters
+        return queryResult
+    }
+}
diff --git a/src/protocol/bolt/src/test/java/org/apache/jmeter/resources/ResourceKeyUsageTestBolt.java b/src/protocol/bolt/src/test/java/org/apache/jmeter/resources/ResourceKeyUsageTestBolt.java
new file mode 100644
index 0000000..ee49316
--- /dev/null
+++ b/src/protocol/bolt/src/test/java/org/apache/jmeter/resources/ResourceKeyUsageTestBolt.java
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.jmeter.resources;
+
+public class ResourceKeyUsageTestBolt extends ResourceKeyUsageTest {
+    // Test from the base class is used, so we just validate current module
+}
diff --git a/src/protocol/build.gradle.kts b/src/protocol/build.gradle.kts
index 88b8da0..959d26d 100644
--- a/src/protocol/build.gradle.kts
+++ b/src/protocol/build.gradle.kts
@@ -23,6 +23,15 @@ subprojects {
     }
 }
 
+project("bolt") {
+    dependencies {
+        implementation("org.neo4j.driver:neo4j-java-driver")
+        implementation("org.apache.commons:commons-lang3")
+        implementation("com.fasterxml.jackson.core:jackson-core")
+        implementation("com.fasterxml.jackson.core:jackson-databind")
+    }
+}
+
 project("ftp") {
     dependencies {
         implementation("commons-net:commons-net:3.6")
diff --git a/xdocs/images/screenshots/bolt-connection-config.png b/xdocs/images/screenshots/bolt-connection-config.png
new file mode 100644
index 0000000..3afe34a
Binary files /dev/null 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
new file mode 100644
index 0000000..521772e
Binary files /dev/null and b/xdocs/images/screenshots/bolt-request.png differ
diff --git a/xdocs/usermanual/component_reference.xml b/xdocs/usermanual/component_reference.xml
index 819cb05..5fa5c32 100644
--- a/xdocs/usermanual/component_reference.xml
+++ b/xdocs/usermanual/component_reference.xml
@@ -1921,6 +1921,39 @@ MongoDB Script is more suitable for functional testing or test setup (setup/tear
 
 <a href="#">^</a>
 
+
+<component name="Bolt Request" index="&sect-num;.1.22" width="711" height="488" screenshot="bolt-request.png">
+    <description>
+        <p>This sampler allows you to run Cypher queries through the Bolt protocol.</p>
+        <p>Before using this you need to set up a <complink name="Bolt Connection Configuration"/></p>
+        <p>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.</p>
+        <p>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.</p>
+    </description>
+
+    <properties>
+        <property name="Name" required="No">Descriptive name for this sampler that is shown in the tree.</property>
+        <property name="Comments" required="No">Free text for additional details.</property>
+        <property name="Cypher statement" required="Yes">
+            The query to execute.
+        </property>
+        <property name="Params" required="No">The parameter values, JSON formatted.</property>
+        <property name="Record Query Results" required="No">
+            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.
+        </property>
+    </properties>
+
+    <note>It is strongly advised to use query parameters, allowing the database to cache and reuse execution plans.</note>
+
+    <links>
+        <complink name="Bolt Connection Configuration"/>
+    </links>
+</component>
+
+<a href="#">^</a>
+
 </section>
 
 <section name="&sect-num;.2 Logic Controllers" anchor="logic_controllers">
@@ -4400,6 +4433,22 @@ DB db = MongoDBHolder.getDBFromSource("value of property MongoDB Source",
 
 <a href="#">^</a>
 
+<component name="Bolt Connection Configuration" index="&sect-num;.4.21"
+           width="711" height="170" screenshot="bolt-connection-config.png">
+    <description>Creates a Bolt connection pool (used by <complink name="Bolt Request"/> Sampler)
+        from the supplied Connection settings.
+    </description>
+    <properties>
+        <property name="Name" required="No">Descriptive name for this sampler that is shown in the tree.</property>
+        <property name="Comments" required="No">Free text for additional details.</property>
+        <property name="Bolt URI" required="Yes">The database URI.</property>
+        <property name="Username" required="No">User account.</property>
+        <property name="Password" required="No">User credentials.</property>
+    </properties>
+</component>
+
+<a href="#">^</a>
+
 </section>
 
 <section name="&sect-num;.5 Assertions" anchor="assertions">