From 0cb0b500cdfd129933bf414466099d19e3b23451 Mon Sep 17 00:00:00 2001 From: Dmitriy Tverdiakov <11927660+injectives@users.noreply.github.com> Date: Mon, 5 Feb 2024 16:48:02 +0000 Subject: [PATCH] Introduce Benchkit backend (#1533) --- benchkit-backend/LICENSES.txt | 250 +++++++++++++++ benchkit-backend/NOTICE.txt | 38 +++ benchkit-backend/pom.xml | 90 ++++++ .../neo4j/org/testkit/backend/Config.java | 42 +++ .../neo4j/org/testkit/backend/Runner.java | 67 ++++ .../channel/handler/HttpRequestHandler.java | 96 ++++++ .../testkit/backend/handler/ReadyHandler.java | 52 ++++ .../backend/handler/WorkloadHandler.java | 286 ++++++++++++++++++ .../backend/request/WorkloadRequest.java | 36 +++ benchkit/Dockerfile | 7 + pom.xml | 1 + 11 files changed, 965 insertions(+) create mode 100644 benchkit-backend/LICENSES.txt create mode 100644 benchkit-backend/NOTICE.txt create mode 100644 benchkit-backend/pom.xml create mode 100644 benchkit-backend/src/main/java/neo4j/org/testkit/backend/Config.java create mode 100644 benchkit-backend/src/main/java/neo4j/org/testkit/backend/Runner.java create mode 100644 benchkit-backend/src/main/java/neo4j/org/testkit/backend/channel/handler/HttpRequestHandler.java create mode 100644 benchkit-backend/src/main/java/neo4j/org/testkit/backend/handler/ReadyHandler.java create mode 100644 benchkit-backend/src/main/java/neo4j/org/testkit/backend/handler/WorkloadHandler.java create mode 100644 benchkit-backend/src/main/java/neo4j/org/testkit/backend/request/WorkloadRequest.java create mode 100644 benchkit/Dockerfile diff --git a/benchkit-backend/LICENSES.txt b/benchkit-backend/LICENSES.txt new file mode 100644 index 0000000000..836ba67225 --- /dev/null +++ b/benchkit-backend/LICENSES.txt @@ -0,0 +1,250 @@ +This file contains the full license text of the included third party +libraries. For an overview of the licenses see the NOTICE.txt file. + + +------------------------------------------------------------------------------ +Apache Software License, Version 2.0 + Jackson-annotations + Jackson-core + jackson-databind + Netty/Buffer + Netty/Codec + Netty/Codec/HTTP + Netty/Common + Netty/Handler + Netty/Resolver + Netty/TomcatNative [OpenSSL - Classes] + Netty/Transport + Netty/Transport/Native/Unix/Common + Non-Blocking Reactive Foundation for the JVM +------------------------------------------------------------------------------ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed 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. + + + +------------------------------------------------------------------------------ +MIT No Attribution License + reactive-streams +------------------------------------------------------------------------------ + +MIT No Attribution + +Copyright + +Permission is hereby granted, free of charge, to any person obtaining a copy of this +software and associated documentation files (the "Software"), to deal in the Software +without restriction, including without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + + diff --git a/benchkit-backend/NOTICE.txt b/benchkit-backend/NOTICE.txt new file mode 100644 index 0000000000..d1f14e0749 --- /dev/null +++ b/benchkit-backend/NOTICE.txt @@ -0,0 +1,38 @@ +Copyright (c) "Neo4j" +Neo4j Sweden AB [https://neo4j.com] + +This file is part of Neo4j. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. + +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. + +Full license texts are found in LICENSES.txt. + + +Third-party licenses +-------------------- + +Apache Software License, Version 2.0 + Jackson-annotations + Jackson-core + jackson-databind + Netty/Buffer + Netty/Codec + Netty/Codec/HTTP + Netty/Common + Netty/Handler + Netty/Resolver + Netty/TomcatNative [OpenSSL - Classes] + Netty/Transport + Netty/Transport/Native/Unix/Common + Non-Blocking Reactive Foundation for the JVM + +MIT No Attribution License + reactive-streams + diff --git a/benchkit-backend/pom.xml b/benchkit-backend/pom.xml new file mode 100644 index 0000000000..14178ed321 --- /dev/null +++ b/benchkit-backend/pom.xml @@ -0,0 +1,90 @@ + + + 4.0.0 + + + neo4j-java-driver-parent + org.neo4j.driver + 5.17-SNAPSHOT + + + benchkit-backend + + Neo4j Java Driver Benchkit Backend + Integration component for use with Benchkit + https://github.com/neo4j/neo4j-java-driver + + + ${project.basedir}/.. + ,-processing + + + + + org.neo4j.driver + neo4j-java-driver + ${project.version} + + + io.netty + netty-handler + + + io.netty + netty-codec-http + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-databind + + + org.projectlombok + lombok + + + + + org.junit.jupiter + junit-jupiter + test + + + + + + + org.apache.maven.plugins + maven-shade-plugin + + + package + + shade + + + + + neo4j.org.testkit.backend.Runner + + + benchkit-backend + + + + + + + + + scm:git:git://github.com/neo4j/neo4j-java-driver.git + scm:git:git@github.com:neo4j/neo4j-java-driver.git + https://github.com/neo4j/neo4j-java-driver + + + diff --git a/benchkit-backend/src/main/java/neo4j/org/testkit/backend/Config.java b/benchkit-backend/src/main/java/neo4j/org/testkit/backend/Config.java new file mode 100644 index 0000000000..56f0e461e8 --- /dev/null +++ b/benchkit-backend/src/main/java/neo4j/org/testkit/backend/Config.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * Licensed 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 neo4j.org.testkit.backend; + +import java.net.URI; +import java.util.logging.Level; +import org.neo4j.driver.AuthToken; +import org.neo4j.driver.AuthTokens; +import org.neo4j.driver.Logging; + +public record Config(int port, URI uri, AuthToken authToken, Logging logging) { + static Config load() { + var env = System.getenv(); + var port = Integer.parseInt(env.getOrDefault("TEST_BACKEND_PORT", "9000")); + var neo4jHost = env.getOrDefault("TEST_NEO4J_HOST", "localhost"); + var neo4jPort = Integer.parseInt(env.getOrDefault("TEST_NEO4J_PORT", "7687")); + var neo4jScheme = env.getOrDefault("TEST_NEO4J_SCHEME", "neo4j"); + var neo4jUser = env.getOrDefault("TEST_NEO4J_USER", "neo4j"); + var neo4jPassword = env.getOrDefault("TEST_NEO4J_PASS", "password"); + var level = env.get("TEST_BACKEND_LOGGING_LEVEL"); + var logging = level == null || level.isEmpty() ? Logging.none() : Logging.console(Level.parse(level)); + return new Config( + port, + URI.create(String.format("%s://%s:%d", neo4jScheme, neo4jHost, neo4jPort)), + AuthTokens.basic(neo4jUser, neo4jPassword), + logging); + } +} diff --git a/benchkit-backend/src/main/java/neo4j/org/testkit/backend/Runner.java b/benchkit-backend/src/main/java/neo4j/org/testkit/backend/Runner.java new file mode 100644 index 0000000000..ee1948f60f --- /dev/null +++ b/benchkit-backend/src/main/java/neo4j/org/testkit/backend/Runner.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * Licensed 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 neo4j.org.testkit.backend; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpServerCodec; +import java.util.concurrent.Executors; +import neo4j.org.testkit.backend.channel.handler.HttpRequestHandler; +import neo4j.org.testkit.backend.handler.ReadyHandler; +import neo4j.org.testkit.backend.handler.WorkloadHandler; +import org.neo4j.driver.GraphDatabase; + +public class Runner { + public static void main(String[] args) throws InterruptedException { + var config = Config.load(); + var driver = GraphDatabase.driver( + config.uri(), + config.authToken(), + org.neo4j.driver.Config.builder().withLogging(config.logging()).build()); + + EventLoopGroup group = new NioEventLoopGroup(); + var logging = config.logging(); + var executor = Executors.newCachedThreadPool(); + var workloadHandler = new WorkloadHandler(driver, executor, logging); + var readyHandler = new ReadyHandler(driver, logging); + try { + var bootstrap = new ServerBootstrap(); + bootstrap + .group(group) + .channel(NioServerSocketChannel.class) + .localAddress(config.port()) + .childHandler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel channel) { + var pipeline = channel.pipeline(); + pipeline.addLast("codec", new HttpServerCodec()); + pipeline.addLast("aggregator", new HttpObjectAggregator(512 * 1024)); + pipeline.addLast(new HttpRequestHandler(workloadHandler, readyHandler, logging)); + } + }); + var server = bootstrap.bind().sync(); + server.channel().closeFuture().sync(); + } finally { + group.shutdownGracefully().sync(); + } + } +} diff --git a/benchkit-backend/src/main/java/neo4j/org/testkit/backend/channel/handler/HttpRequestHandler.java b/benchkit-backend/src/main/java/neo4j/org/testkit/backend/channel/handler/HttpRequestHandler.java new file mode 100644 index 0000000000..1e2e85b9e3 --- /dev/null +++ b/benchkit-backend/src/main/java/neo4j/org/testkit/backend/channel/handler/HttpRequestHandler.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * Licensed 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 neo4j.org.testkit.backend.channel.handler; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpUtil; +import io.netty.handler.codec.http.HttpVersion; +import java.nio.charset.StandardCharsets; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.CompletionStage; +import neo4j.org.testkit.backend.handler.ReadyHandler; +import neo4j.org.testkit.backend.handler.WorkloadHandler; +import neo4j.org.testkit.backend.request.WorkloadRequest; +import org.neo4j.driver.Logger; +import org.neo4j.driver.Logging; + +public class HttpRequestHandler extends SimpleChannelInboundHandler { + private final ObjectMapper objectMapper = new ObjectMapper(); + private final WorkloadHandler workloadHandler; + private final ReadyHandler readyHandler; + private final Logger logger; + + public HttpRequestHandler(WorkloadHandler workloadHandler, ReadyHandler readyHandler, Logging logging) { + this.workloadHandler = Objects.requireNonNull(workloadHandler); + this.readyHandler = Objects.requireNonNull(readyHandler); + this.logger = logging.getLog(getClass()); + } + + @Override + public void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) { + if (HttpUtil.is100ContinueExpected(request)) { + send100Continue(ctx); + } + + CompletionStage responseStage; + if ("/workload".equals(request.uri()) && HttpMethod.PUT.equals(request.method())) { + var content = request.content().toString(StandardCharsets.UTF_8); + responseStage = CompletableFuture.completedStage(null) + .thenApply(ignored -> { + try { + return objectMapper.readValue(content, WorkloadRequest.class); + } catch (JsonProcessingException e) { + throw new CompletionException(e); + } + }) + .thenCompose(workloadRequest -> workloadHandler.handle(request.protocolVersion(), workloadRequest)); + } else if ("/ready".equals(request.uri()) && HttpMethod.GET.equals(request.method())) { + responseStage = readyHandler.ready(request.protocolVersion()); + } else { + logger.warn("Unknown request %s with %s method.", request.uri(), request.method()); + responseStage = CompletableFuture.completedFuture( + new DefaultFullHttpResponse(request.protocolVersion(), HttpResponseStatus.INTERNAL_SERVER_ERROR)); + } + + responseStage.whenComplete((response, throwable) -> { + response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8"); + ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); + }); + } + + private static void send100Continue(ChannelHandlerContext ctx) { + ctx.writeAndFlush(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE)); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + logger.error("An unexpected error occured.", cause); + ctx.close(); + } +} diff --git a/benchkit-backend/src/main/java/neo4j/org/testkit/backend/handler/ReadyHandler.java b/benchkit-backend/src/main/java/neo4j/org/testkit/backend/handler/ReadyHandler.java new file mode 100644 index 0000000000..96ba83720a --- /dev/null +++ b/benchkit-backend/src/main/java/neo4j/org/testkit/backend/handler/ReadyHandler.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * Licensed 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 neo4j.org.testkit.backend.handler; + +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpVersion; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import org.neo4j.driver.Driver; +import org.neo4j.driver.Logger; +import org.neo4j.driver.Logging; + +public class ReadyHandler { + private final Driver driver; + private final Logger logger; + + public ReadyHandler(Driver driver, Logging logging) { + this.driver = driver; + this.logger = logging.getLog(getClass()); + } + + public CompletionStage ready(HttpVersion httpVersion) { + return CompletableFuture.completedStage(null) + .thenComposeAsync(ignored -> driver.verifyConnectivityAsync()) + .handle((ignored, throwable) -> { + HttpResponseStatus status; + if (throwable != null) { + logger.error("An error occured during workload handling.", throwable); + status = HttpResponseStatus.INTERNAL_SERVER_ERROR; + } else { + status = HttpResponseStatus.NO_CONTENT; + } + return new DefaultFullHttpResponse(httpVersion, status); + }); + } +} diff --git a/benchkit-backend/src/main/java/neo4j/org/testkit/backend/handler/WorkloadHandler.java b/benchkit-backend/src/main/java/neo4j/org/testkit/backend/handler/WorkloadHandler.java new file mode 100644 index 0000000000..6cf062a9a8 --- /dev/null +++ b/benchkit-backend/src/main/java/neo4j/org/testkit/backend/handler/WorkloadHandler.java @@ -0,0 +1,286 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * Licensed 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 neo4j.org.testkit.backend.handler; + +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpVersion; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; +import java.util.stream.Stream; +import neo4j.org.testkit.backend.request.WorkloadRequest; +import org.neo4j.driver.AccessMode; +import org.neo4j.driver.Driver; +import org.neo4j.driver.Logger; +import org.neo4j.driver.Logging; +import org.neo4j.driver.QueryConfig; +import org.neo4j.driver.RoutingControl; +import org.neo4j.driver.Session; +import org.neo4j.driver.SessionConfig; +import org.neo4j.driver.SimpleQueryRunner; +import org.neo4j.driver.TransactionCallback; + +public class WorkloadHandler { + private final Driver driver; + private final Executor executor; + private final Logger logger; + + public WorkloadHandler(Driver driver, Executor executor, Logging logging) { + this.driver = Objects.requireNonNull(driver); + this.executor = Objects.requireNonNull(executor); + this.logger = logging.getLog(getClass()); + } + + public CompletionStage handle(HttpVersion httpVersion, WorkloadRequest workloadRequest) { + return CompletableFuture.completedStage(null) + .thenComposeAsync( + ignored -> switch (workloadRequest.getMethod()) { + case "executeQuery" -> executeQuery(workloadRequest); + case "sessionRun" -> sessionRun(workloadRequest); + case "executeRead", "executeWrite" -> execute(workloadRequest); + default -> CompletableFuture.failedStage( + new IllegalArgumentException("Unknown workload type.")); + }, + executor) + .handle((ignored, throwable) -> { + HttpResponseStatus status; + if (throwable != null) { + logger.error("An error occured during workload handling.", throwable); + status = HttpResponseStatus.INTERNAL_SERVER_ERROR; + } else { + status = HttpResponseStatus.NO_CONTENT; + } + return new DefaultFullHttpResponse(httpVersion, status); + }); + } + + private CompletionStage executeQuery(WorkloadRequest workloadRequest) { + var routingControl = + switch (workloadRequest.getRouting()) { + case "read" -> RoutingControl.READ; + case "write" -> RoutingControl.WRITE; + default -> null; + }; + if (routingControl == null) { + return CompletableFuture.failedStage(new IllegalArgumentException("Unknown routing.")); + } + return switch (workloadRequest.getMode()) { + case "sequentialSessions" -> runAsStage(() -> executeQueriesSequentially( + workloadRequest.getQueries(), workloadRequest.getDatabase(), routingControl)); + case "parallelSessions" -> executeQueriesConcurrently( + workloadRequest.getQueries(), workloadRequest.getDatabase(), routingControl); + default -> CompletableFuture.failedStage(new IllegalArgumentException("Unknown workload type.")); + }; + } + + private void executeQueriesSequentially( + List queries, String database, RoutingControl routingControl) { + for (var query : queries) { + executeQuery(query.getText(), query.getParameters(), database, routingControl); + } + } + + private CompletionStage executeQueriesConcurrently( + List queries, String database, RoutingControl routingControl) { + return runAsStage( + queries.stream() + .map(query -> + () -> executeQuery(query.getText(), query.getParameters(), database, routingControl)), + executor); + } + + @SuppressWarnings("unchecked") + private void executeQuery(String query, Map parameters, String database, RoutingControl routingControl) { + var configBuilder = QueryConfig.builder().withRouting(routingControl); + if (database != null) { + configBuilder.withDatabase(database); + } + driver.executableQuery(query) + .withParameters((Map) parameters) + .withConfig(configBuilder.build()) + .execute(); + } + + private CompletionStage sessionRun(WorkloadRequest workloadRequest) { + var accessMode = + switch (workloadRequest.getRouting()) { + case "read" -> AccessMode.READ; + case "write" -> AccessMode.WRITE; + default -> null; + }; + if (accessMode == null) { + return CompletableFuture.failedStage(new IllegalArgumentException("Unknown routing.")); + } + return switch (workloadRequest.getMode()) { + case "sequentialSessions" -> runAsStage(() -> + runInMultipleSessions(workloadRequest.getQueries(), workloadRequest.getDatabase(), accessMode)); + case "sequentialTransactions" -> runAsStage( + () -> runInSingleSession(workloadRequest.getQueries(), workloadRequest.getDatabase(), accessMode)); + case "parallelSessions" -> runInConcurrentSessions( + workloadRequest.getQueries(), workloadRequest.getDatabase(), accessMode); + default -> CompletableFuture.failedStage(new IllegalArgumentException("Unknown workload type.")); + }; + } + + private void runInMultipleSessions(List queries, String database, AccessMode accessMode) { + for (var query : queries) { + try (var session = driver.session(sessionConfigBuilder(database) + .withDefaultAccessMode(accessMode) + .build())) { + run(session, query.getText(), query.getParameters()); + } + } + } + + private void runInSingleSession(List queries, String database, AccessMode accessMode) { + try (var session = driver.session( + sessionConfigBuilder(database).withDefaultAccessMode(accessMode).build())) { + for (var query : queries) { + run(session, query.getText(), query.getParameters()); + } + } + } + + private CompletionStage runInConcurrentSessions( + List queries, String database, AccessMode accessMode) { + return runAsStage( + queries.stream() + .map(query -> () -> runInSingleSession(Collections.singletonList(query), database, accessMode)), + executor); + } + + private void run(SimpleQueryRunner queryRunner, String query, Map parameters) { + @SuppressWarnings("unchecked") + var result = queryRunner.run(query, (Map) parameters); + while (result.hasNext()) { + result.next(); + } + } + + private CompletionStage execute(WorkloadRequest workloadRequest) { + BiConsumer> runner = + switch (workloadRequest.getRouting()) { + case "read" -> Session::executeRead; + case "write" -> Session::executeWrite; + default -> null; + }; + if (runner == null) { + return CompletableFuture.failedStage(new IllegalArgumentException("Unknown routing.")); + } + return switch (workloadRequest.getMode()) { + case "sequentialSessions" -> runAsStage(() -> + executeInMultipleSessions(runner, workloadRequest.getQueries(), workloadRequest.getDatabase())); + case "sequentialTransactions" -> runAsStage( + () -> executeSingleSession(runner, workloadRequest.getQueries(), workloadRequest.getDatabase())); + case "sequentialQueries" -> runAsStage(() -> + executeInSingleTransaction(runner, workloadRequest.getQueries(), workloadRequest.getDatabase())); + case "parallelSessions" -> executeConcurrently( + runner, workloadRequest.getQueries(), workloadRequest.getDatabase()); + default -> CompletableFuture.failedStage(new IllegalArgumentException("Unknown workload type.")); + }; + } + + private void executeInMultipleSessions( + BiConsumer> runner, + List queries, + String database) { + for (var query : queries) { + try (var session = driver.session(sessionConfigBuilder(database).build())) { + runner.accept(session, tx -> { + run(tx, query.getText(), query.getParameters()); + return null; + }); + } + } + } + + private void executeSingleSession( + BiConsumer> runner, + List queries, + String database) { + try (var session = driver.session(sessionConfigBuilder(database).build())) { + for (var query : queries) { + runner.accept(session, tx -> { + run(tx, query.getText(), query.getParameters()); + return null; + }); + } + } + } + + private void executeInSingleTransaction( + BiConsumer> runner, + List queries, + String database) { + var configBuilder = SessionConfig.builder(); + if (database != null) { + configBuilder.withDatabase(database); + } + try (var session = driver.session(configBuilder.build())) { + runner.accept(session, tx -> { + for (var query : queries) { + run(tx, query.getText(), query.getParameters()); + } + return null; + }); + } + } + + private CompletionStage executeConcurrently( + BiConsumer> runner, + List queries, + String database) { + return runAsStage( + queries.stream() + .map(query -> () -> executeSingleSession(runner, Collections.singletonList(query), database)), + executor); + } + + private CompletionStage runAsStage(Runnable runnable) { + var future = new CompletableFuture(); + try { + runnable.run(); + future.complete(null); + } catch (Throwable throwable) { + future.completeExceptionally(throwable); + } + return future; + } + + private CompletionStage runAsStage(Stream runnables, Executor executor) { + return CompletableFuture.allOf(runnables + .map(runnable -> CompletableFuture.runAsync(runnable, executor)) + .toArray(CompletableFuture[]::new)) + .orTimeout(1, TimeUnit.MINUTES); + } + + private SessionConfig.Builder sessionConfigBuilder(String database) { + var configBuilder = SessionConfig.builder(); + if (database != null) { + configBuilder.withDatabase(database); + } + return configBuilder; + } +} diff --git a/benchkit-backend/src/main/java/neo4j/org/testkit/backend/request/WorkloadRequest.java b/benchkit-backend/src/main/java/neo4j/org/testkit/backend/request/WorkloadRequest.java new file mode 100644 index 0000000000..4ee9330b43 --- /dev/null +++ b/benchkit-backend/src/main/java/neo4j/org/testkit/backend/request/WorkloadRequest.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * Licensed 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 neo4j.org.testkit.backend.request; + +import java.util.List; +import java.util.Map; +import lombok.Data; + +@Data +public class WorkloadRequest { + String method; + List queries; + String database; + String routing; + String mode; + + @Data + public static class Query { + String text; + Map parameters; + } +} diff --git a/benchkit/Dockerfile b/benchkit/Dockerfile new file mode 100644 index 0000000000..4fbf5efdbd --- /dev/null +++ b/benchkit/Dockerfile @@ -0,0 +1,7 @@ +FROM maven:3.9.2-eclipse-temurin-17 as build +COPY . /driver +RUN cd /driver && mvn --show-version --batch-mode clean install -P !determine-revision -DskipTests + +FROM eclipse-temurin:17-jre +COPY --from=build /driver/benchkit-backend/target/benchkit-backend.jar /benchkit-backend.jar +CMD java -jar benchkit-backend.jar diff --git a/pom.xml b/pom.xml index bcc46ff58d..3a2339900d 100644 --- a/pom.xml +++ b/pom.xml @@ -68,6 +68,7 @@ examples testkit-backend testkit-tests + benchkit-backend