diff --git a/.github/workflows/java.yml b/.github/workflows/java.yml index ad55bc368d..45bf72aefb 100644 --- a/.github/workflows/java.yml +++ b/.github/workflows/java.yml @@ -40,6 +40,7 @@ jobs: sudo apt install -y protobuf-compiler mkdir -p java/client/src/main/java/org/babushka/javababushka/generated protoc -Iprotobuf=babushka-core/src/protobuf/ --java_out=java/client/src/main/java/org/babushka/javababushka/generated babushka-core/src/protobuf/*.proto + - name: Build rust part working-directory: java run: cargo build diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index d13870c92e..0000000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - // Use IntelliSense to find out which attributes exist for C# debugging - // Use hover for the description of the existing attributes - // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md - "name": ".NET Core Launch (console)", - "type": "coreclr", - "request": "launch", - "preLaunchTask": "build", - // If you have changed target frameworks, make sure to update the program path. - "program": "${workspaceFolder}/csharp/tests/bin/Debug/net6.0/tests.dll", - "args": [], - "cwd": "${workspaceFolder}/csharp/tests", - // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console - "console": "internalConsole", - "stopAtEntry": false - }, - { - "name": ".NET Core Attach", - "type": "coreclr", - "request": "attach" - }, - { - "name": "C# benchmark Launch (console)", - "type": "coreclr", - "request": "launch", - "preLaunchTask": "build", - "program": "${workspaceFolder}/benchmarks/csharp/bin/Debug/net6.0/csharp_benchmark.dll", - "args": [], - "cwd": "${workspaceFolder}/benchmarks/csharp", - "console": "internalConsole", - "stopAtEntry": true - }, - { - "name": ".NET Core Attach", - "type": "coreclr", - "request": "attach" - } - ] -} diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 70ac6dd315..0000000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "editor.insertSpaces": true, - "editor.tabSize": 4, - "editor.codeActionsOnSave": { - "source.organizeImports": true - }, - "editor.formatOnSave": true, - "files.insertFinalNewline": true, - "files.trimFinalNewlines": true, - "python.formatting.provider": "black", - "rust-analyzer.linkedProjects": [ - "babushka-core/Cargo.toml", - "python/Cargo.toml", - "node/rust-client/Cargo.toml", - "logger_core/Cargo.toml", - "csharp/lib/Cargo.toml", - "submodules/redis-rs/Cargo.toml", - "benchmarks/rust/Cargo.toml" - ], - "rust-analyzer.runnableEnv": { - "REDISRS_SERVER_TYPE": "tcp" - }, - "python.testing.pytestArgs": ["python"], - "python.testing.unittestEnabled": false, - "python.testing.pytestEnabled": true, - "[yaml]": { - "editor.tabSize": 4 - }, - "python.linting.flake8Enabled": false, - "python.linting.enabled": true, - "python.linting.flake8Args": [ - "--extend-ignore=E203", - "--max-line-length=127" - ], - "python.formatting.blackArgs": ["--target-version", "py36"], - "isort.args": ["--profile", "black"] -} diff --git a/.vscode/tasks.json b/.vscode/tasks.json deleted file mode 100644 index f5f11a93a9..0000000000 --- a/.vscode/tasks.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "version": "2.0.0", - "tasks": [ - { - "label": "build", - "command": "dotnet", - "type": "process", - "args": [ - "build", - "${workspaceFolder}/csharp/tests/tests.csproj", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "problemMatcher": "$msCompile" - }, - { - "label": "publish", - "command": "dotnet", - "type": "process", - "args": [ - "publish", - "${workspaceFolder}/csharp/tests/tests.csproj", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "problemMatcher": "$msCompile" - }, - { - "label": "watch", - "command": "dotnet", - "type": "process", - "args": [ - "watch", - "run", - "--project", - "${workspaceFolder}/csharp/tests/tests.csproj" - ], - "problemMatcher": "$msCompile" - } - ] -} \ No newline at end of file diff --git a/benchmarks/install_and_test.sh b/benchmarks/install_and_test.sh index 2a7853a412..86953ad5fb 100755 --- a/benchmarks/install_and_test.sh +++ b/benchmarks/install_and_test.sh @@ -24,12 +24,14 @@ runAllBenchmarks=1 runPython=0 runNode=0 runCsharp=0 +runJava=0 runRust=0 concurrentTasks="1 10 100 1000" dataSize="100 4000" clientCount="1" chosenClients="all" host="localhost" +port=6379 tlsFlag="--tls" function runPythonBenchmark(){ @@ -68,6 +70,22 @@ function runCSharpBenchmark(){ dotnet run --configuration Release --resultsFile=../$1 --dataSize $2 --concurrentTasks $concurrentTasks --clients $chosenClients --host $host --clientCount $clientCount $tlsFlag $portFlag } +function runJavaBenchmark(){ + cd ${BENCH_FOLDER}/../java + echo "./gradlew run --args=\"--resultsFile=${BENCH_FOLDER}/$1 --clients $chosenClients --host $host --port $port\"" +# ./gradlew run --args="--resultsFile=../$1 --dataSize $2 --concurrentTasks $concurrentTasks --clients $chosenClients --host $host --port $port --clientCount $clientCount $tlsFlag" + ./gradlew run --args="--resultsFile=${BENCH_FOLDER}/$1 --clients $chosenClients --host $host --port $port" + cd ${BENCH_FOLDER}/java +} + +function runJavaBenchmark(){ + cd ${BENCH_FOLDER}/../java + echo "./gradlew run --args=\"--resultsFile=${BENCH_FOLDER}/$1 --clients $chosenClients --host $host --port $port\"" +# ./gradlew run --args="--resultsFile=../$1 --dataSize $2 --concurrentTasks $concurrentTasks --clients $chosenClients --host $host --port $port --clientCount $clientCount $tlsFlag" + ./gradlew run --args="--resultsFile=${BENCH_FOLDER}/$1 --clients $chosenClients --host $host --port $port" + cd ${BENCH_FOLDER}/java +} + function runRustBenchmark(){ rustConcurrentTasks= for value in $concurrentTasks @@ -185,6 +203,36 @@ do runAllBenchmarks=0 runNode=1 ;; + -java) + runAllBenchmarks=0 + runJava=1 + chosenClients="Babushka" + ;; + -lettuce) + runAllBenchmarks=0 + runJava=1 + chosenClients="Lettuce" + ;; + -lettuce) + runAllBenchmarks=0 + runJava=1 + chosenClients="Jedis" + ;; + -java) + runAllBenchmarks=0 + runJava=1 + chosenClients="Babushka" + ;; + -lettuce) + runAllBenchmarks=0 + runJava=1 + chosenClients="Lettuce" + ;; + -lettuce) + runAllBenchmarks=0 + runJava=1 + chosenClients="Jedis" + ;; -csharp) runAllBenchmarks=0 runCsharp=1 @@ -242,6 +290,13 @@ do runCSharpBenchmark $csharpResults $currentDataSize fi + if [ $runAllBenchmarks == 1 ] || [ $runJava == 1 ]; + then + javaResults=$(resultFileName java $currentDataSize) + resultFiles+=$javaResults" " + runJavaBenchmark $javaResults $currentDataSize + fi + if [ $runAllBenchmarks == 1 ] || [ $runRust == 1 ]; then rustResults=$(resultFileName rust $currentDataSize) @@ -250,8 +305,6 @@ do fi done - - flushDB if [ $writeResultsCSV == 1 ]; diff --git a/java/Cargo.toml b/java/Cargo.toml index 8046f3578c..01ecb23e4c 100644 --- a/java/Cargo.toml +++ b/java/Cargo.toml @@ -1,4 +1,3 @@ - [package] name = "javababushka" version = "0.0.0" diff --git a/java/README.md b/java/README.md index 12ae1966f2..4664866806 100644 --- a/java/README.md +++ b/java/README.md @@ -33,6 +33,7 @@ You can run benchmarks using `./gradlew run`. You can set arguments using the ar ``` The following arguments are accepted: +* `configuration`: Release or Debug configuration * `resultsFile`: the results output file * `concurrentTasks`: Number of concurrent tasks * `clients`: one of: all|jedis|lettuce|babushka @@ -46,4 +47,3 @@ The following arguments are accepted: * Connection Timeout: * If you're unable to connect to redis, check that you are connecting to the correct host, port, and TLS configuration. * Only server-side certificates are supported by the TLS configured redis. - diff --git a/java/benchmarks/src/main/java/javababushka/benchmarks/AsyncClient.java b/java/benchmarks/src/main/java/javababushka/benchmarks/AsyncClient.java new file mode 100644 index 0000000000..a688775084 --- /dev/null +++ b/java/benchmarks/src/main/java/javababushka/benchmarks/AsyncClient.java @@ -0,0 +1,16 @@ +package javababushka.benchmarks; + +import java.util.concurrent.Future; + +public interface AsyncClient extends Client { + + long DEFAULT_TIMEOUT = 1000; + + Future asyncSet(String key, String value); + + Future asyncGet(String key); + + T waitForResult(Future future); + + T waitForResult(Future future, long timeout); +} diff --git a/java/benchmarks/src/main/java/javababushka/benchmarks/BenchmarkingApp.java b/java/benchmarks/src/main/java/javababushka/benchmarks/BenchmarkingApp.java new file mode 100644 index 0000000000..66ea1f5029 --- /dev/null +++ b/java/benchmarks/src/main/java/javababushka/benchmarks/BenchmarkingApp.java @@ -0,0 +1,239 @@ +package javababushka.benchmarks; + +import static javababushka.benchmarks.utils.Benchmarking.testClientSetGet; + +import java.io.FileWriter; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javababushka.benchmarks.jedis.JedisClient; +import javababushka.benchmarks.jedis.JedisPseudoAsyncClient; +import javababushka.benchmarks.lettuce.LettuceAsyncClient; +import javababushka.benchmarks.lettuce.LettuceClient; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; + +/** Benchmarking app for reporting performance of various redis-rs Java-clients */ +public class BenchmarkingApp { + + // main application entrypoint + public static void main(String[] args) { + + // create the parser + CommandLineParser parser = new DefaultParser(); + Options options = getOptions(); + RunConfiguration runConfiguration = new RunConfiguration(); + try { + // parse the command line arguments + CommandLine line = parser.parse(options, args); + runConfiguration = verifyOptions(line); + } catch (ParseException exp) { + // oops, something went wrong + System.err.println("Parsing failed. Reason: " + exp.getMessage()); + } + + for (ClientName client : runConfiguration.clients) { + switch (client) { + case JEDIS: + testClientSetGet(JedisClient::new, runConfiguration, false); + break; + case JEDIS_ASYNC: + testClientSetGet(JedisPseudoAsyncClient::new, runConfiguration, true); + break; + case LETTUCE: + testClientSetGet(LettuceClient::new, runConfiguration, false); + break; + case LETTUCE_ASYNC: + testClientSetGet(LettuceAsyncClient::new, runConfiguration, true); + break; + case BABUSHKA: + System.out.println("Babushka not yet configured"); + break; + } + } + + if (runConfiguration.resultsFile.isPresent()) { + try { + runConfiguration.resultsFile.get().close(); + } catch (IOException ioException) { + System.out.println("Error closing results file"); + } + } + } + + private static Options getOptions() { + // create the Options + Options options = new Options(); + + options.addOption("c", "configuration", true, "Configuration flag [Release]"); + options.addOption("f", "resultsFile", true, "Result filepath []"); + options.addOption("d", "dataSize", true, "Data block size [20]"); + options.addOption("C", "concurrentTasks", true, "Number of concurrent tasks [1 10 100]"); + options.addOption( + "l", "clients", true, "one of: all|jedis|jedis_async|lettuce|lettuce_async|babushka [all]"); + options.addOption("h", "host", true, "host url [localhost]"); + options.addOption("p", "port", true, "port number [6379]"); + options.addOption("n", "clientCount", true, "Client count [1]"); + options.addOption("t", "tls", false, "TLS [true]"); + + return options; + } + + private static RunConfiguration verifyOptions(CommandLine line) throws ParseException { + RunConfiguration runConfiguration = new RunConfiguration(); + if (line.hasOption("configuration")) { + String configuration = line.getOptionValue("configuration"); + if (configuration.equalsIgnoreCase("Release") || configuration.equalsIgnoreCase("Debug")) { + runConfiguration.configuration = configuration; + } else { + throw new ParseException("Invalid run configuration (Release|Debug)"); + } + } + + if (line.hasOption("resultsFile")) { + try { + runConfiguration.resultsFile = + Optional.of(new FileWriter(line.getOptionValue("resultsFile"))); + } catch (IOException e) { + throw new ParseException("Unable to write to resultsFile."); + } + } + + if (line.hasOption("dataSize")) { + runConfiguration.dataSize = Integer.parseInt(line.getOptionValue("dataSize")); + } + + if (line.hasOption("concurrentTasks")) { + String concurrentTasks = line.getOptionValue("concurrentTasks"); + + // remove optional square brackets + if (concurrentTasks.startsWith("[") && concurrentTasks.endsWith("]")) { + concurrentTasks = concurrentTasks.substring(1, concurrentTasks.length() - 1); + } + // check if it's the correct format + if (!concurrentTasks.matches("\\d+(\\s+\\d+)?")) { + throw new ParseException("Invalid concurrentTasks"); + } + // split the string into a list of integers + runConfiguration.concurrentTasks = + Arrays.stream(concurrentTasks.split("\\s+")) + .map(Integer::parseInt) + .collect(Collectors.toList()); + } + + if (line.hasOption("clients")) { + String[] clients = line.getOptionValue("clients").split(","); + runConfiguration.clients = + Arrays.stream(clients) + .map(c -> Enum.valueOf(ClientName.class, c.toUpperCase())) + .flatMap( + e -> { + switch (e) { + case ALL: + return Stream.of( + ClientName.JEDIS, + ClientName.JEDIS_ASYNC, + ClientName.BABUSHKA, + ClientName.LETTUCE, + ClientName.LETTUCE_ASYNC); + case ALL_ASYNC: + return Stream.of( + ClientName.JEDIS_ASYNC, + // ClientName.BABUSHKA, + ClientName.LETTUCE_ASYNC); + case ALL_SYNC: + return Stream.of( + ClientName.JEDIS, + // ClientName.BABUSHKA, + ClientName.LETTUCE); + default: + return Stream.of(e); + } + }) + .toArray(ClientName[]::new); + } + + if (line.hasOption("host")) { + runConfiguration.host = line.getOptionValue("host"); + } + + if (line.hasOption("clientCount")) { + String clientCount = line.getOptionValue("clientCount"); + + // check if it's the correct format + if (!clientCount.matches("\\d+(\\s+\\d+)?")) { + throw new ParseException("Invalid concurrentTasks"); + } + // split the string into a list of integers + runConfiguration.clientCount = + Arrays.stream(clientCount.split("\\s+")).mapToInt(Integer::parseInt).toArray(); + } + + if (line.hasOption("tls")) { + runConfiguration.tls = Boolean.parseBoolean(line.getOptionValue("tls")); + } + + return runConfiguration; + } + + public enum ClientName { + JEDIS("Jedis"), + JEDIS_ASYNC("Jedis async"), + LETTUCE("Lettuce"), + LETTUCE_ASYNC("Lettuce async"), + BABUSHKA("Babushka"), + ALL("All"), + ALL_SYNC("All sync"), + ALL_ASYNC("All async"); + + private String name; + + private ClientName(String name) { + this.name = name; + } + + @Override + public String toString() { + return this.name; + } + + public boolean isEqual(String other) { + return this.toString().equalsIgnoreCase(other); + } + } + + public static class RunConfiguration { + public String configuration; + public Optional resultsFile; + public int dataSize; + public List concurrentTasks; + public ClientName[] clients; + public String host; + public int port; + public int[] clientCount; + public boolean tls; + public boolean debugLogging = false; + + public RunConfiguration() { + configuration = "Release"; + resultsFile = Optional.empty(); + dataSize = 20; + concurrentTasks = List.of(10, 100); + clients = + new ClientName[] { + // ClientName.BABUSHKA, + ClientName.JEDIS, ClientName.JEDIS_ASYNC, ClientName.LETTUCE, ClientName.LETTUCE_ASYNC + }; + host = "localhost"; + port = 6379; + clientCount = new int[] {1, 2}; + tls = false; + } + } +} diff --git a/java/benchmarks/src/main/java/javababushka/benchmarks/Client.java b/java/benchmarks/src/main/java/javababushka/benchmarks/Client.java new file mode 100644 index 0000000000..16ab36847d --- /dev/null +++ b/java/benchmarks/src/main/java/javababushka/benchmarks/Client.java @@ -0,0 +1,13 @@ +package javababushka.benchmarks; + +import javababushka.benchmarks.utils.ConnectionSettings; + +public interface Client { + void connectToRedis(); + + void connectToRedis(ConnectionSettings connectionSettings); + + default void closeConnection() {} + + String getName(); +} diff --git a/java/benchmarks/src/main/java/javababushka/benchmarks/SyncClient.java b/java/benchmarks/src/main/java/javababushka/benchmarks/SyncClient.java new file mode 100644 index 0000000000..c99174af3d --- /dev/null +++ b/java/benchmarks/src/main/java/javababushka/benchmarks/SyncClient.java @@ -0,0 +1,7 @@ +package javababushka.benchmarks; + +public interface SyncClient extends Client { + void set(String key, String value); + + String get(String key); +} diff --git a/java/benchmarks/src/main/java/javababushka/benchmarks/jedis/JedisClient.java b/java/benchmarks/src/main/java/javababushka/benchmarks/jedis/JedisClient.java new file mode 100644 index 0000000000..214a6cae10 --- /dev/null +++ b/java/benchmarks/src/main/java/javababushka/benchmarks/jedis/JedisClient.java @@ -0,0 +1,65 @@ +/* + * This Java source file was generated by the Gradle 'init' task. + */ +package javababushka.benchmarks.jedis; + +import javababushka.benchmarks.SyncClient; +import javababushka.benchmarks.utils.ConnectionSettings; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; + +public class JedisClient implements SyncClient { + + public static final String DEFAULT_HOST = "localhost"; + public static final int DEFAULT_PORT = 6379; + + protected Jedis jedisResource; + + public boolean someLibraryMethod() { + return true; + } + + @Override + public void connectToRedis() { + JedisPool pool = new JedisPool(DEFAULT_HOST, DEFAULT_PORT); + jedisResource = pool.getResource(); + } + + @Override + public void closeConnection() { + try { + jedisResource.close(); + } catch (Exception ignored) { + } + } + + @Override + public String getName() { + return "Jedis"; + } + + @Override + public void connectToRedis(ConnectionSettings connectionSettings) { + jedisResource = + new Jedis(connectionSettings.host, connectionSettings.port, connectionSettings.useSsl); + jedisResource.connect(); + } + + public String info() { + return jedisResource.info(); + } + + public String info(String section) { + return jedisResource.info(section); + } + + @Override + public void set(String key, String value) { + jedisResource.set(key, value); + } + + @Override + public String get(String key) { + return jedisResource.get(key); + } +} diff --git a/java/benchmarks/src/main/java/javababushka/benchmarks/jedis/JedisPseudoAsyncClient.java b/java/benchmarks/src/main/java/javababushka/benchmarks/jedis/JedisPseudoAsyncClient.java new file mode 100644 index 0000000000..598c455d1e --- /dev/null +++ b/java/benchmarks/src/main/java/javababushka/benchmarks/jedis/JedisPseudoAsyncClient.java @@ -0,0 +1,39 @@ +package javababushka.benchmarks.jedis; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import javababushka.benchmarks.AsyncClient; + +// Jedis doesn't provide async API +// https://github.com/redis/jedis/issues/241 +public class JedisPseudoAsyncClient extends JedisClient implements AsyncClient { + @Override + public Future asyncSet(String key, String value) { + return CompletableFuture.runAsync(() -> super.set(key, value)); + } + + @Override + public Future asyncGet(String key) { + return CompletableFuture.supplyAsync(() -> super.get(key)); + } + + @Override + public T waitForResult(Future future) { + return waitForResult(future, DEFAULT_TIMEOUT); + } + + @Override + public T waitForResult(Future future, long timeout) { + try { + return future.get(timeout, TimeUnit.MILLISECONDS); + } catch (Exception ignored) { + return null; + } + } + + @Override + public String getName() { + return "Jedis pseudo-async"; + } +} diff --git a/java/benchmarks/src/main/java/javababushka/benchmarks/lettuce/LettuceAsyncClient.java b/java/benchmarks/src/main/java/javababushka/benchmarks/lettuce/LettuceAsyncClient.java new file mode 100644 index 0000000000..1b8c4ba9b7 --- /dev/null +++ b/java/benchmarks/src/main/java/javababushka/benchmarks/lettuce/LettuceAsyncClient.java @@ -0,0 +1,73 @@ +/* + * This Java source file was generated by the Gradle 'init' task. + */ +package javababushka.benchmarks.lettuce; + +import io.lettuce.core.RedisClient; +import io.lettuce.core.RedisFuture; +import io.lettuce.core.api.StatefulRedisConnection; +import io.lettuce.core.api.async.RedisAsyncCommands; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import javababushka.benchmarks.AsyncClient; +import javababushka.benchmarks.utils.ConnectionSettings; + +public class LettuceAsyncClient implements AsyncClient { + + RedisClient client; + RedisAsyncCommands asyncCommands; + StatefulRedisConnection connection; + + @Override + public void connectToRedis() { + connectToRedis(new ConnectionSettings("localhost", 6379, false)); + } + + @Override + public void connectToRedis(ConnectionSettings connectionSettings) { + client = + RedisClient.create( + String.format( + "%s://%s:%d", + connectionSettings.useSsl ? "rediss" : "redis", + connectionSettings.host, + connectionSettings.port)); + connection = client.connect(); + asyncCommands = connection.async(); + } + + @Override + public RedisFuture asyncSet(String key, String value) { + return asyncCommands.set(key, value); + } + + @Override + public RedisFuture asyncGet(String key) { + return asyncCommands.get(key); + } + + @Override + public Object waitForResult(Future future) { + return waitForResult(future, DEFAULT_TIMEOUT); + } + + @Override + public Object waitForResult(Future future, long timeoutMS) { + try { + return future.get(timeoutMS, TimeUnit.MILLISECONDS); + } catch (Exception ignored) { + return null; + } + } + + @Override + public void closeConnection() { + connection.close(); + client.shutdown(); + } + + @Override + public String getName() { + return "Lettuce Async"; + } +} diff --git a/java/benchmarks/src/main/java/javababushka/benchmarks/lettuce/LettuceClient.java b/java/benchmarks/src/main/java/javababushka/benchmarks/lettuce/LettuceClient.java new file mode 100644 index 0000000000..e4e1830bda --- /dev/null +++ b/java/benchmarks/src/main/java/javababushka/benchmarks/lettuce/LettuceClient.java @@ -0,0 +1,56 @@ +/* + * This Java source file was generated by the Gradle 'init' task. + */ +package javababushka.benchmarks.lettuce; + +import io.lettuce.core.RedisClient; +import io.lettuce.core.api.StatefulRedisConnection; +import io.lettuce.core.api.sync.RedisStringCommands; +import javababushka.benchmarks.SyncClient; +import javababushka.benchmarks.utils.ConnectionSettings; + +public class LettuceClient implements SyncClient { + + RedisClient client; + RedisStringCommands syncCommands; + StatefulRedisConnection connection; + + @Override + public void connectToRedis() { + connectToRedis(new ConnectionSettings("localhost", 6379, false)); + } + + @Override + public void connectToRedis(ConnectionSettings connectionSettings) { + client = + RedisClient.create( + String.format( + "%s://%s:%d", + connectionSettings.useSsl ? "rediss" : "redis", + connectionSettings.host, + connectionSettings.port)); + connection = client.connect(); + syncCommands = connection.sync(); + } + + @Override + public void set(String key, String value) { + syncCommands.set(key, value); + } + + @Override + public String get(String key) { + return (String) syncCommands.get(key); + } + + @Override + public void closeConnection() { + connection.close(); + client.shutdown(); + } + + @Override + public String getName() { + return "Lettuce"; + } +} diff --git a/java/benchmarks/src/main/java/javababushka/benchmarks/utils/Benchmarking.java b/java/benchmarks/src/main/java/javababushka/benchmarks/utils/Benchmarking.java new file mode 100644 index 0000000000..14818f46b4 --- /dev/null +++ b/java/benchmarks/src/main/java/javababushka/benchmarks/utils/Benchmarking.java @@ -0,0 +1,257 @@ +package javababushka.benchmarks.utils; + +import java.io.FileWriter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import javababushka.benchmarks.AsyncClient; +import javababushka.benchmarks.BenchmarkingApp; +import javababushka.benchmarks.Client; +import javababushka.benchmarks.SyncClient; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.tuple.Pair; + +public class Benchmarking { + static final double PROB_GET = 0.8; + static final double PROB_GET_EXISTING_KEY = 0.8; + static final int SIZE_GET_KEYSPACE = 3750000; + static final int SIZE_SET_KEYSPACE = 3000000; + static final int ASYNC_OPERATION_TIMEOUT_SEC = 1; + + private static ChosenAction randomAction() { + if (Math.random() > PROB_GET) { + return ChosenAction.SET; + } + if (Math.random() > PROB_GET_EXISTING_KEY) { + return ChosenAction.GET_NON_EXISTING; + } + return ChosenAction.GET_EXISTING; + } + + public static String generateKeyGet() { + int range = SIZE_GET_KEYSPACE - SIZE_SET_KEYSPACE; + return Math.floor(Math.random() * range + SIZE_SET_KEYSPACE + 1) + ""; + } + + public static String generateKeySet() { + return (Math.floor(Math.random() * SIZE_SET_KEYSPACE) + 1) + ""; + } + + public interface Operation { + void go() throws Exception; + } + + private static Pair getLatency(Map actions) { + var action = randomAction(); + long before = System.nanoTime(); + try { + actions.get(action).go(); + } catch (Exception e) { + // timed out - exception from Future::get + } + long after = System.nanoTime(); + return Pair.of(action, after - before); + } + + // Assumption: latencies is sorted in ascending order + private static Long percentile(ArrayList latencies, int percentile) { + int N = latencies.size(); + double n = (N - 1) * percentile / 100. + 1; + if (n == 1d) return latencies.get(0); + else if (n == N) return latencies.get(N - 1); + int k = (int) n; + double d = n - k; + return Math.round(latencies.get(k - 1) + d * (latencies.get(k) - latencies.get(k - 1))); + } + + private static double stdDeviation(ArrayList latencies, Double avgLatency) { + double stdDeviation = + latencies.stream() + .mapToDouble(Long::doubleValue) + .reduce(0.0, (stdDev, latency) -> stdDev + Math.pow(latency - avgLatency, 2)); + return Math.sqrt(stdDeviation / latencies.size()); + } + + // This has the side-effect of sorting each latencies ArrayList + public static Map calculateResults( + Map> actionLatencies) { + Map results = new HashMap(); + + for (Map.Entry> entry : actionLatencies.entrySet()) { + ChosenAction action = entry.getKey(); + ArrayList latencies = entry.getValue(); + + Double avgLatency = + latencies.stream().collect(Collectors.summingLong(Long::longValue)) + / Double.valueOf(latencies.size()); + + Collections.sort(latencies); + results.put( + action, + new LatencyResults( + avgLatency, + percentile(latencies, 50), + percentile(latencies, 90), + percentile(latencies, 99), + stdDeviation(latencies, avgLatency))); + } + + return results; + } + + public static void printResults( + Map calculatedResults, Optional resultsFile) { + if (resultsFile.isPresent()) { + printResults(calculatedResults, resultsFile.get()); + } else { + printResults(calculatedResults); + } + } + + public static void printResults( + Map resultsMap, FileWriter resultsFile) { + for (Map.Entry entry : resultsMap.entrySet()) { + ChosenAction action = entry.getKey(); + LatencyResults results = entry.getValue(); + + try { + resultsFile.write("Avg. time in ms per " + action + ": " + results.avgLatency / 1000000.0); + resultsFile.write(action + " p50 latency in ms: " + results.p50Latency / 1000000.0); + resultsFile.write(action + " p90 latency in ms: " + results.p90Latency / 1000000.0); + resultsFile.write(action + " p99 latency in ms: " + results.p99Latency / 1000000.0); + resultsFile.write(action + " std dev in ms: " + results.stdDeviation / 1000000.0); + } catch (Exception ignored) { + } + } + } + + public static void printResults(Map resultsMap) { + for (Map.Entry entry : resultsMap.entrySet()) { + ChosenAction action = entry.getKey(); + LatencyResults results = entry.getValue(); + + System.out.println("Avg. time in ms per " + action + ": " + results.avgLatency / 1000000.0); + System.out.println(action + " p50 latency in ms: " + results.p50Latency / 1000000.0); + System.out.println(action + " p90 latency in ms: " + results.p90Latency / 1000000.0); + System.out.println(action + " p99 latency in ms: " + results.p99Latency / 1000000.0); + System.out.println(action + " std dev in ms: " + results.stdDeviation / 1000000.0); + } + } + + public static void testClientSetGet( + Supplier clientCreator, BenchmarkingApp.RunConfiguration config, boolean async) { + for (int concurrentNum : config.concurrentTasks) { + int iterations = Math.min(Math.max(100000, concurrentNum * 10000), 10000000); + for (int clientCount : config.clientCount) { + System.out.printf( + "%n =====> %s <===== %d clients %d concurrent %n%n", + clientCreator.get().getName(), clientCount, concurrentNum); + AtomicInteger iterationCounter = new AtomicInteger(0); + Map> actionResults = + Map.of( + ChosenAction.GET_EXISTING, new ArrayList<>(), + ChosenAction.GET_NON_EXISTING, new ArrayList<>(), + ChosenAction.SET, new ArrayList<>()); + List tasks = new ArrayList<>(); + + // create clients + List clients = new LinkedList<>(); + for (int cc = 0; cc < clientCount; cc++) { + Client newClient = clientCreator.get(); + newClient.connectToRedis(new ConnectionSettings(config.host, config.port, config.tls)); + clients.add(newClient); + } + + for (int taskNum = 0; taskNum < concurrentNum; taskNum++) { + final int taskNumDebugging = taskNum; + tasks.add( + () -> { + int iterationIncrement = iterationCounter.getAndIncrement(); + int clientIndex = iterationIncrement % clients.size(); + + if (config.debugLogging) { + System.out.printf( + "%n concurrent = %d/%d, client# = %d/%d%n", + taskNumDebugging, concurrentNum, clientIndex + 1, clientCount); + } + while (iterationIncrement < iterations) { + if (config.debugLogging) { + System.out.printf( + "> iteration = %d/%d, client# = %d/%d%n", + iterationIncrement + 1, iterations, clientIndex + 1, clientCount); + } + // operate and calculate tik-tok + Pair result = + measurePerformance(clients.get(clientIndex), config.dataSize, async); + actionResults.get(result.getLeft()).add(result.getRight()); + + iterationIncrement = iterationCounter.getAndIncrement(); + } + }); + } + if (config.debugLogging) { + System.out.printf("%s client Benchmarking: %n", clientCreator.get().getName()); + System.out.printf( + "===> concurrentNum = %d, clientNum = %d, tasks = %d%n", + concurrentNum, clientCount, tasks.size()); + } + tasks.stream() + .map(CompletableFuture::runAsync) + .forEach( + f -> { + try { + f.get(); + } catch (Exception e) { + e.printStackTrace(); + } + }); + + printResults(calculateResults(actionResults), config.resultsFile); + } + } + + System.out.println(); + } + + public static Pair measurePerformance( + Client client, int dataSize, boolean async) { + + String value = RandomStringUtils.randomAlphanumeric(dataSize); + Map actions = new HashMap<>(); + actions.put( + ChosenAction.GET_EXISTING, + async + ? () -> + ((AsyncClient) client) + .asyncGet(generateKeySet()) + .get(ASYNC_OPERATION_TIMEOUT_SEC, TimeUnit.SECONDS) + : () -> ((SyncClient) client).get(generateKeySet())); + actions.put( + ChosenAction.GET_NON_EXISTING, + async + ? () -> + ((AsyncClient) client) + .asyncGet(generateKeyGet()) + .get(ASYNC_OPERATION_TIMEOUT_SEC, TimeUnit.SECONDS) + : () -> ((SyncClient) client).get(generateKeyGet())); + actions.put( + ChosenAction.SET, + async + ? () -> + ((AsyncClient) client) + .asyncSet(generateKeySet(), value) + .get(ASYNC_OPERATION_TIMEOUT_SEC, TimeUnit.SECONDS) + : () -> ((SyncClient) client).set(generateKeySet(), value)); + + return getLatency(actions); + } +} diff --git a/java/benchmarks/src/main/java/javababushka/benchmarks/utils/ChosenAction.java b/java/benchmarks/src/main/java/javababushka/benchmarks/utils/ChosenAction.java new file mode 100644 index 0000000000..bd9f01fd90 --- /dev/null +++ b/java/benchmarks/src/main/java/javababushka/benchmarks/utils/ChosenAction.java @@ -0,0 +1,7 @@ +package javababushka.benchmarks.utils; + +public enum ChosenAction { + GET_NON_EXISTING, + GET_EXISTING, + SET +} diff --git a/java/benchmarks/src/main/java/javababushka/benchmarks/utils/ConnectionSettings.java b/java/benchmarks/src/main/java/javababushka/benchmarks/utils/ConnectionSettings.java new file mode 100644 index 0000000000..2989d8b3b3 --- /dev/null +++ b/java/benchmarks/src/main/java/javababushka/benchmarks/utils/ConnectionSettings.java @@ -0,0 +1,13 @@ +package javababushka.benchmarks.utils; + +public class ConnectionSettings { + public String host; + public int port; + public boolean useSsl; + + public ConnectionSettings(String host, int port, boolean useSsl) { + this.host = host; + this.port = port; + this.useSsl = useSsl; + } +} diff --git a/java/benchmarks/src/main/java/javababushka/benchmarks/utils/LatencyResults.java b/java/benchmarks/src/main/java/javababushka/benchmarks/utils/LatencyResults.java new file mode 100644 index 0000000000..8e2baa3a2b --- /dev/null +++ b/java/benchmarks/src/main/java/javababushka/benchmarks/utils/LatencyResults.java @@ -0,0 +1,19 @@ +package javababushka.benchmarks.utils; + +// Raw timing results in nanoseconds +public class LatencyResults { + public final double avgLatency; + public final long p50Latency; + public final long p90Latency; + public final long p99Latency; + public final double stdDeviation; + + public LatencyResults( + double avgLatency, long p50Latency, long p90Latency, long p99Latency, double stdDeviation) { + this.avgLatency = avgLatency; + this.p50Latency = p50Latency; + this.p90Latency = p90Latency; + this.p99Latency = p99Latency; + this.stdDeviation = stdDeviation; + } +} diff --git a/java/benchmarks/src/test/java/javababushka/benchmarks/jedis/JedisClientIT.java b/java/benchmarks/src/test/java/javababushka/benchmarks/jedis/JedisClientIT.java new file mode 100644 index 0000000000..d1669d36e0 --- /dev/null +++ b/java/benchmarks/src/test/java/javababushka/benchmarks/jedis/JedisClientIT.java @@ -0,0 +1,58 @@ +/* + * This Java source file was generated by the Gradle 'init' task. + */ +package javababushka.benchmarks.jedis; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.HashMap; +import java.util.Map; +import javababushka.benchmarks.utils.Benchmarking; +import javababushka.benchmarks.utils.ChosenAction; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class JedisClientIT { + + private static JedisClient jedisClient; + + @BeforeAll + static void initializeJedisClient() { + jedisClient = new JedisClient(); + jedisClient.connectToRedis(); + } + + @Test + public void someLibraryMethodReturnsTrue() { + JedisClient classUnderTest = new JedisClient(); + assertTrue(classUnderTest.someLibraryMethod(), "someLibraryMethod should return 'true'"); + } + + @Test + public void testResourceInfo() { + String result = jedisClient.info(); + + assertTrue(result.length() > 0); + } + + @Test + public void testResourceInfoBySection() { + String section = "Server"; + String result = jedisClient.info(section); + + assertTrue(result.length() > 0); + assertTrue(result.startsWith("# " + section)); + } + + @Test + public void testResourceSetGet() { + int iterations = 100000; + String value = "my-value"; + + Map actions = new HashMap<>(); + actions.put(ChosenAction.GET_EXISTING, () -> jedisClient.get(Benchmarking.generateKeySet())); + actions.put( + ChosenAction.GET_NON_EXISTING, () -> jedisClient.get(Benchmarking.generateKeyGet())); + actions.put(ChosenAction.SET, () -> jedisClient.set(Benchmarking.generateKeySet(), value)); + } +} diff --git a/java/benchmarks/src/test/java/javababushka/benchmarks/lettuce/LettuceAsyncClientIT.java b/java/benchmarks/src/test/java/javababushka/benchmarks/lettuce/LettuceAsyncClientIT.java new file mode 100644 index 0000000000..c051c1b2a4 --- /dev/null +++ b/java/benchmarks/src/test/java/javababushka/benchmarks/lettuce/LettuceAsyncClientIT.java @@ -0,0 +1,77 @@ +/* + * This Java source file was generated by the Gradle 'init' task. + */ +package javababushka.benchmarks.lettuce; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +import io.lettuce.core.RedisFuture; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class LettuceAsyncClientIT { + + private static LettuceAsyncClient lettuceClient; + + private static LettuceAsyncClient otherLettuceClient; + + @BeforeAll + static void initializeJedisClient() { + lettuceClient = new LettuceAsyncClient(); + lettuceClient.connectToRedis(); + + otherLettuceClient = new LettuceAsyncClient(); + otherLettuceClient.connectToRedis(); + } + + @AfterAll + static void closeConnection() { + lettuceClient.closeConnection(); + otherLettuceClient.closeConnection(); + } + + @Test + public void testResourceSetGet() { + String key = "key1"; + String value = "my-value-1"; + + String otherKey = "key2"; + String otherValue = "my-value-2"; + + RedisFuture setResult = lettuceClient.asyncSet(key, value); + RedisFuture otherSetResult = otherLettuceClient.asyncSet(otherKey, otherValue); + + // and wait for both clients + try { + lettuceClient.waitForResult(setResult); + } catch (Exception e) { + fail("Can SET redis result without Exception"); + } + try { + otherLettuceClient.waitForResult(otherSetResult); + } catch (Exception e) { + fail("Can SET other redis result without Exception"); + } + + RedisFuture getResult = lettuceClient.asyncGet(key); + RedisFuture otherGetResult = otherLettuceClient.asyncGet(otherKey); + String result = "invalid"; + String otherResult = "invalid"; + try { + result = (String) lettuceClient.waitForResult(getResult); + } catch (Exception e) { + fail("Can GET redis result without Exception"); + } + + try { + otherResult = (String) otherLettuceClient.waitForResult(otherGetResult); + } catch (Exception e) { + fail("Can GET other redis result without Exception"); + } + + assertEquals(value, result); + assertEquals(otherValue, otherResult); + } +} diff --git a/java/benchmarks/src/test/java/javababushka/benchmarks/lettuce/LettuceClientIT.java b/java/benchmarks/src/test/java/javababushka/benchmarks/lettuce/LettuceClientIT.java new file mode 100644 index 0000000000..445cc5b584 --- /dev/null +++ b/java/benchmarks/src/test/java/javababushka/benchmarks/lettuce/LettuceClientIT.java @@ -0,0 +1,39 @@ +/* + * This Java source file was generated by the Gradle 'init' task. + */ +package javababushka.benchmarks.lettuce; + +import java.util.HashMap; +import javababushka.benchmarks.utils.Benchmarking; +import javababushka.benchmarks.utils.ChosenAction; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class LettuceClientIT { + + private static LettuceClient lettuceClient; + + @BeforeAll + static void initializeJedisClient() { + lettuceClient = new LettuceClient(); + lettuceClient.connectToRedis(); + } + + @AfterAll + static void closeConnection() { + lettuceClient.closeConnection(); + } + + @Test + public void testResourceSetGet() { + int iterations = 100000; + String value = "my-value"; + + HashMap actions = new HashMap<>(); + actions.put(ChosenAction.GET_EXISTING, () -> lettuceClient.get(Benchmarking.generateKeySet())); + actions.put( + ChosenAction.GET_NON_EXISTING, () -> lettuceClient.get(Benchmarking.generateKeyGet())); + actions.put(ChosenAction.SET, () -> lettuceClient.set(Benchmarking.generateKeySet(), value)); + } +} diff --git a/java/client/build.gradle b/java/client/build.gradle index a8de4b1b6e..d99f81ba7a 100644 --- a/java/client/build.gradle +++ b/java/client/build.gradle @@ -44,4 +44,3 @@ tasks.register('buildAll') { dependsOn 'protobuf', 'buildRust' finalizedBy 'build' } -