diff --git a/performance/README.md b/performance/README.md new file mode 100644 index 00000000..a28fa85f --- /dev/null +++ b/performance/README.md @@ -0,0 +1,79 @@ +# Spring Data Valkey Performance Tests + +Performance benchmarks for Spring Data Valkey operations across different clients. + +## Prerequisites + +- JDK 17 or higher +- Maven 3.9.9 or higher (use `../mvnw` from root directory) +- Valkey server running on `localhost:6379` (or configure connection in tests) + +If using a development build of Spring Data Valkey, first install to your local Maven repository before running the tests: +```bash +# From project root +$ ./mvnw clean install -DskipTests +``` + +See instructions on starting a Valkey server using the `Makefile` in the root [README](../README.md#building-from-source). The standalone instance started by the Makefile is used in these tests. + +## Running Tests + +### Template Performance Test + +Test ValkeyTemplate operations (`SET`, `GET`, `DELETE`) with different clients: + +```bash +$ mvn -q compile exec:java -Dclient=valkeyglide +$ mvn -q compile exec:java -Dclient=lettuce +$ mvn -q compile exec:java -Dclient=jedis +``` + +### Multi-Threaded Performance Test + +Test template use across mulitple threads with different clients: + +```bash +$ mvn -q compile exec:java@threaded-test -Dclient=valkeyglide +$ mvn -q compile exec:java@threaded-test -Dclient=lettuce +$ mvn -q compile exec:java@threaded-test -Dclient=jedis +``` + +### Direct Client Performance Test + +Test direct client operations without Spring Data Valkey (for comparison): + +```bash +$ mvn -q compile exec:java@direct-test -Dclient=valkeyglide +$ mvn -q compile exec:java@direct-test -Dclient=lettuce +$ mvn -q compile exec:java@direct-test -Dclient=jedis +``` + +### Multi-Threaded Direct Client Performance Test + +Test direct client operations across multiple threads: + +```bash +$ mvn -q compile exec:java@threaded-direct-test -Dclient=valkeyglide +$ mvn -q compile exec:java@threaded-direct-test -Dclient=lettuce +$ mvn -q compile exec:java@threaded-direct-test -Dclient=jedis +``` + +### Template Load Test + +Test ValkeyTemplate operations (`SET`, `GET`, `DELETE`) with different clients and concurrency levels. + +Parameters: +- `client`: Client type - `valkeyglide`, `lettuce`, `jedis` (default: `valkeyglide`) +- `threads`: Number of threads (default: `10`) +- `operations`: Operations per thread (default: `50`) + +```bash +# Compare a single client with different concurrency levels +$ mvn -q compile exec:java@load-test -Dclient=valkeyglide -Dthreads=5 -Doperations=20 +$ mvn -q compile exec:java@load-test -Dclient=valkeyglide -Dthreads=20 -Doperations=100 + +# Comapre across different clients +$ mvn -q compile exec:java@load-test -Dclient=valkeyglide -Dthreads=100 -Doperations=200 +$ mvn -q compile exec:java@load-test -Dclient=lettuce -Dthreads=100 -Doperations=200 +$ mvn -q compile exec:java@load-test -Dclient=jedis -Dthreads=100 -Doperations=200 +``` diff --git a/performance/pom.xml b/performance/pom.xml new file mode 100644 index 00000000..cc5b9d56 --- /dev/null +++ b/performance/pom.xml @@ -0,0 +1,108 @@ + + + 4.0.0 + + io.valkey.springframework.data + spring-data-valkey-performance + 1.0.0 + pom + + Spring Data Valkey - Performance Tests + Performance benchmarks for Spring Data Valkey operations across different clients + + + UTF-8 + 17 + 3.5.1 + 2.1.1 + 6.4.0.RELEASE + 5.2.0 + 2.0.9 + + + + + io.valkey.springframework.data + spring-data-valkey + ${spring-data-valkey.version} + + + io.valkey + valkey-glide + ${valkey-glide.version} + ${os.detected.classifier} + + + io.lettuce + lettuce-core + ${lettuce.version} + + + redis.clients + jedis + ${jedis.version} + + + org.slf4j + slf4j-simple + ${slf4j.version} + + + + + + + kr.motd.maven + os-maven-plugin + 1.7.1 + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + ${maven.compiler.release} + + + + org.codehaus.mojo + exec-maven-plugin + 3.1.0 + + performance.TemplatePerformanceTest + false + + + + threaded-test + + performance.MultiThreadedPerformanceTest + + + + direct-test + + performance.DirectClientPerformanceTest + + + + threaded-direct-test + + performance.ThreadedDirectClientPerformanceTest + + + + load-test + + performance.TemplateLoadTest + + + + + + + diff --git a/performance/src/main/java/performance/DirectClientPerformanceTest.java b/performance/src/main/java/performance/DirectClientPerformanceTest.java new file mode 100644 index 00000000..7bccdbde --- /dev/null +++ b/performance/src/main/java/performance/DirectClientPerformanceTest.java @@ -0,0 +1,151 @@ +/* + * Copyright 2025 the original author or authors. + * + * 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 + * + * https://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 performance; + +import glide.api.GlideClient; +import glide.api.models.GlideString; +import glide.api.models.configuration.GlideClientConfiguration; +import glide.api.models.configuration.NodeAddress; +import io.lettuce.core.RedisClient; +import io.lettuce.core.RedisURI; +import io.lettuce.core.api.StatefulRedisConnection; +import io.lettuce.core.api.sync.RedisCommands; +import redis.clients.jedis.Jedis; + +/** + * Direct client performance test without Spring Data Valkey overhead. + */ +public class DirectClientPerformanceTest { + + private static final int OPERATIONS = 10000; + private static final String KEY_PREFIX = "direct:test:"; + + public static void main(String[] args) throws Exception { + String clientType = System.getProperty("client", "valkeyglide"); + + System.out.println("Running Direct Client Performance Test"); + System.out.println("Client: " + clientType); + System.out.println("Operations: " + OPERATIONS); + System.out.println("----------------------------------------"); + + switch (clientType.toLowerCase()) { + case "valkeyglide" -> testValkeyGlide(); + case "lettuce" -> testLettuce(); + case "jedis" -> testJedis(); + default -> throw new IllegalArgumentException("Unknown client: " + clientType); + } + } + + private static void testValkeyGlide() throws Exception { + GlideClientConfiguration config = GlideClientConfiguration.builder() + .address(NodeAddress.builder().host("localhost").port(6379).build()) + .build(); + + try (GlideClient client = GlideClient.createClient(config).get()) { + // SET operations + long start = System.nanoTime(); + for (int i = 0; i < OPERATIONS; i++) { + client.set(GlideString.of(KEY_PREFIX + i), GlideString.of("value" + i)).get(); + } + long setTime = System.nanoTime() - start; + printResult("SET", setTime); + + // GET operations + start = System.nanoTime(); + for (int i = 0; i < OPERATIONS; i++) { + client.get(GlideString.of(KEY_PREFIX + i)).get(); + } + long getTime = System.nanoTime() - start; + printResult("GET", getTime); + + // DELETE operations + start = System.nanoTime(); + for (int i = 0; i < OPERATIONS; i++) { + client.del(new GlideString[]{GlideString.of(KEY_PREFIX + i)}).get(); + } + long deleteTime = System.nanoTime() - start; + printResult("DELETE", deleteTime); + } + } + + private static void testLettuce() { + RedisClient client = RedisClient.create(RedisURI.create("redis://localhost:6379")); + + try (StatefulRedisConnection connection = client.connect()) { + RedisCommands commands = connection.sync(); + + // SET operations + long start = System.nanoTime(); + for (int i = 0; i < OPERATIONS; i++) { + commands.set(KEY_PREFIX + i, "value" + i); + } + long setTime = System.nanoTime() - start; + printResult("SET", setTime); + + // GET operations + start = System.nanoTime(); + for (int i = 0; i < OPERATIONS; i++) { + commands.get(KEY_PREFIX + i); + } + long getTime = System.nanoTime() - start; + printResult("GET", getTime); + + // DELETE operations + start = System.nanoTime(); + for (int i = 0; i < OPERATIONS; i++) { + commands.del(KEY_PREFIX + i); + } + long deleteTime = System.nanoTime() - start; + printResult("DELETE", deleteTime); + } finally { + client.shutdown(); + } + } + + private static void testJedis() { + try (Jedis jedis = new Jedis("localhost", 6379)) { + // SET operations + long start = System.nanoTime(); + for (int i = 0; i < OPERATIONS; i++) { + jedis.set(KEY_PREFIX + i, "value" + i); + } + long setTime = System.nanoTime() - start; + printResult("SET", setTime); + + // GET operations + start = System.nanoTime(); + for (int i = 0; i < OPERATIONS; i++) { + jedis.get(KEY_PREFIX + i); + } + long getTime = System.nanoTime() - start; + printResult("GET", getTime); + + // DELETE operations + start = System.nanoTime(); + for (int i = 0; i < OPERATIONS; i++) { + jedis.del(KEY_PREFIX + i); + } + long deleteTime = System.nanoTime() - start; + printResult("DELETE", deleteTime); + } + } + + private static void printResult(String operation, long durationNanos) { + long durationMs = durationNanos / 1_000_000; + System.out.printf("%s: %,d ops/sec (%.2f ms total)%n", + operation, (long) (OPERATIONS * 1000.0 / durationMs), durationMs / 1.0); + } +} diff --git a/performance/src/main/java/performance/MultiThreadedPerformanceTest.java b/performance/src/main/java/performance/MultiThreadedPerformanceTest.java new file mode 100644 index 00000000..98311538 --- /dev/null +++ b/performance/src/main/java/performance/MultiThreadedPerformanceTest.java @@ -0,0 +1,147 @@ +/* + * Copyright 2025 the original author or authors. + * + * 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 + * + * https://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 performance; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.IntStream; + +import io.valkey.springframework.data.valkey.connection.ValkeyConnectionFactory; +import io.valkey.springframework.data.valkey.connection.jedis.JedisConnectionFactory; +import io.valkey.springframework.data.valkey.connection.lettuce.LettuceConnectionFactory; +import io.valkey.springframework.data.valkey.connection.valkeyglide.ValkeyGlideConnectionFactory; +import io.valkey.springframework.data.valkey.core.StringValkeyTemplate; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; + +/** + * Multi-threaded performance test for ValkeyTemplate operations across different clients. + */ +public class MultiThreadedPerformanceTest { + + private static final int THREADS = 100; + private static final int OPERATIONS_PER_THREAD = 100; + private static final int TOTAL_OPERATIONS = THREADS * OPERATIONS_PER_THREAD; + + public static void main(String[] args) throws Exception { + String clientType = System.getProperty("client", "valkeyglide"); + + System.out.println("Running Multi-Threaded Performance Test"); + System.out.println("Client: " + clientType); + System.out.println("Threads: " + THREADS); + System.out.println("Operations per thread: " + OPERATIONS_PER_THREAD); + System.out.println("Total operations: " + TOTAL_OPERATIONS); + System.out.println("----------------------------------------"); + + ValkeyConnectionFactory factory = createConnectionFactory(clientType); + if (factory instanceof InitializingBean) { + ((InitializingBean) factory).afterPropertiesSet(); + } + + try { + runMultiThreadedTest(factory); + } finally { + if (factory instanceof DisposableBean) { + ((DisposableBean) factory).destroy(); + } + } + } + + private static ValkeyConnectionFactory createConnectionFactory(String clientType) { + return switch (clientType.toLowerCase()) { + case "lettuce" -> new LettuceConnectionFactory(); + case "jedis" -> new JedisConnectionFactory(); + case "valkeyglide" -> new ValkeyGlideConnectionFactory(); + default -> throw new IllegalArgumentException("Unknown client: " + clientType); + }; + } + + private static void runMultiThreadedTest(ValkeyConnectionFactory factory) throws InterruptedException { + long startTime = System.currentTimeMillis(); + + ExecutorService executorService = Executors.newFixedThreadPool(THREADS); + + StringValkeyTemplate template = new StringValkeyTemplate(factory); + + AtomicInteger setOperations = new AtomicInteger(0); + AtomicInteger getOperations = new AtomicInteger(0); + AtomicInteger deleteOperations = new AtomicInteger(0); + AtomicInteger setFailures = new AtomicInteger(0); + AtomicInteger getFailures = new AtomicInteger(0); + AtomicInteger deleteFailures = new AtomicInteger(0); + + try { + Runnable task = () -> { + try { + IntStream.range(0, OPERATIONS_PER_THREAD).forEach(i -> { + String key = Thread.currentThread().getName() + ":" + i; + String value = "value" + i; + + // SET operation + try { + template.opsForValue().set(key, value); + setOperations.incrementAndGet(); + } catch (Exception e) { + setFailures.incrementAndGet(); + } + + // GET operation + try { + String result = template.opsForValue().get(key); + if (result != null) { + getOperations.incrementAndGet(); + } + } catch (Exception e) { + getFailures.incrementAndGet(); + } + + // DELETE operation + try { + template.delete(key); + deleteOperations.incrementAndGet(); + } catch (Exception e) { + deleteFailures.incrementAndGet(); + } + }); + } catch (Exception e) { + System.err.println("Thread failed: " + e.getMessage()); + setFailures.addAndGet(OPERATIONS_PER_THREAD); + getFailures.addAndGet(OPERATIONS_PER_THREAD); + deleteFailures.addAndGet(OPERATIONS_PER_THREAD); + } + }; + + IntStream.range(0, THREADS).forEach(i -> executorService.submit(task)); + + executorService.shutdown(); + executorService.awaitTermination(10, TimeUnit.SECONDS); + long duration = System.currentTimeMillis() - startTime; + + printResult("SET", duration, setOperations.get(), setFailures.get()); + printResult("GET", duration, getOperations.get(), getFailures.get()); + printResult("DELETE", duration, deleteOperations.get(), deleteFailures.get()); + } finally { + executorService.shutdown(); + } + } + + private static void printResult(String operation, long duration, int successful, int failed) { + System.out.printf("%s: %,d ops/sec (%.2f ms total), %.1f%% successful%n", + operation, (long) (successful * 1000.0 / duration), duration / 1.0, (successful * 100.0 / TOTAL_OPERATIONS)); + } +} diff --git a/performance/src/main/java/performance/TemplateLoadTest.java b/performance/src/main/java/performance/TemplateLoadTest.java new file mode 100644 index 00000000..54c02f9f --- /dev/null +++ b/performance/src/main/java/performance/TemplateLoadTest.java @@ -0,0 +1,129 @@ +/* + * Copyright 2025 the original author or authors. + * + * 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 + * + * https://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 performance; + +import io.valkey.springframework.data.valkey.connection.ValkeyConnectionFactory; +import io.valkey.springframework.data.valkey.connection.jedis.JedisConnectionFactory; +import io.valkey.springframework.data.valkey.connection.lettuce.LettuceConnectionFactory; +import io.valkey.springframework.data.valkey.connection.valkeyglide.ValkeyGlideConnectionFactory; +import io.valkey.springframework.data.valkey.core.StringValkeyTemplate; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.IntStream; + +/** + * Parameterized load test for ValkeyTemplate using different clients and under various concurrency levels. + */ +public class TemplateLoadTest { + + public static void main(String[] args) throws Exception { + String client = System.getProperty("client", "valkeyglide"); + int threads = Integer.parseInt(System.getProperty("threads", "10")); + int operations = Integer.parseInt(System.getProperty("operations", "50")); + int totalExpected = threads * operations * 2; // SET + GET + + System.out.println("Running Template Load Test"); + System.out.println("Client: " + client); + System.out.println("Threads: " + threads); + System.out.println("Operations per thread: " + operations); + System.out.println("Total operations: " + totalExpected); + System.out.println("----------------------------------------"); + + ValkeyConnectionFactory factory = createConnectionFactory(client); + if (factory instanceof InitializingBean) { + ((InitializingBean) factory).afterPropertiesSet(); + } + + try { + runLoadTest(factory, threads, operations, totalExpected); + } finally { + if (factory instanceof DisposableBean) { + ((DisposableBean) factory).destroy(); + } + } + } + + private static ValkeyConnectionFactory createConnectionFactory(String clientType) { + return switch (clientType.toLowerCase()) { + case "lettuce" -> new LettuceConnectionFactory(); + case "jedis" -> new JedisConnectionFactory(); + case "valkeyglide" -> new ValkeyGlideConnectionFactory(); + default -> throw new IllegalArgumentException("Unknown client: " + clientType); + }; + } + + private static void runLoadTest(ValkeyConnectionFactory factory, int threads, + int operations, int totalExpected) throws InterruptedException { + long startTime = System.currentTimeMillis(); + + ExecutorService executorService = Executors.newFixedThreadPool(threads); + StringValkeyTemplate valkeyTemplate = new StringValkeyTemplate(factory); + + AtomicInteger setOperations = new AtomicInteger(0); + AtomicInteger getOperations = new AtomicInteger(0); + + try { + Runnable task = () -> IntStream.range(0, operations).forEach(i -> { + String key = Thread.currentThread().getName() + ":" + i; + String value = "value" + i; + + // SET operation + try { + valkeyTemplate.opsForValue().set(key, value); + setOperations.incrementAndGet(); + } catch (Exception e) { + System.err.println("SET error: " + e.getMessage()); + } + + // GET operation + try { + String result = valkeyTemplate.opsForValue().get(key); + getOperations.incrementAndGet(); + if (result != null && !result.equals(value)) { + System.err.println("Data mismatch! Expected: " + value + ", Got: " + result); + } + } catch (Exception e) { + System.err.println("GET error: " + e.getMessage()); + } + }); + + IntStream.range(0, threads).forEach(i -> executorService.submit(task)); + + executorService.shutdown(); + executorService.awaitTermination(10, TimeUnit.SECONDS); + + long duration = System.currentTimeMillis() - startTime; + int totalActual = setOperations.get() + getOperations.get(); + int dropped = totalExpected - totalActual; + + System.out.println("Duration: " + String.format("%.2f ms", (double) duration)); + System.out.println("Expected operations: " + totalExpected); + System.out.println("SET operations: " + setOperations.get()); + System.out.println("GET operations: " + getOperations.get()); + System.out.println("Total operations: " + totalActual); + System.out.println("Dropped operations: " + dropped); + System.out.println("Success rate: " + String.format("%.2f%%", (totalActual * 100.0 / totalExpected))); + System.out.println("Operations per second: " + String.format("%,d", (long) (totalActual * 1000.0 / duration))); + } finally { + executorService.shutdown(); + } + } +} diff --git a/performance/src/main/java/performance/TemplatePerformanceTest.java b/performance/src/main/java/performance/TemplatePerformanceTest.java new file mode 100644 index 00000000..00257297 --- /dev/null +++ b/performance/src/main/java/performance/TemplatePerformanceTest.java @@ -0,0 +1,97 @@ +/* + * Copyright 2025 the original author or authors. + * + * 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 + * + * https://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 performance; + +import io.valkey.springframework.data.valkey.connection.ValkeyConnectionFactory; +import io.valkey.springframework.data.valkey.connection.jedis.JedisConnectionFactory; +import io.valkey.springframework.data.valkey.connection.lettuce.LettuceConnectionFactory; +import io.valkey.springframework.data.valkey.connection.valkeyglide.ValkeyGlideConnectionFactory; +import io.valkey.springframework.data.valkey.core.StringValkeyTemplate; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.DisposableBean; + +/** + * Performance test for ValkeyTemplate operations across different clients. + */ +public class TemplatePerformanceTest { + + private static final int OPERATIONS = 10000; + private static final String KEY_PREFIX = "perf:test:"; + + public static void main(String[] args) throws Exception { + String clientType = System.getProperty("client", "valkeyglide"); + + System.out.println("Running ValkeyTemplate Performance Test"); + System.out.println("Client: " + clientType); + System.out.println("Operations: " + OPERATIONS); + System.out.println("----------------------------------------"); + + ValkeyConnectionFactory factory = createConnectionFactory(clientType); + if (factory instanceof InitializingBean) { + ((InitializingBean) factory).afterPropertiesSet(); + } + + try { + StringValkeyTemplate template = new StringValkeyTemplate(factory); + runPerformanceTest(template); + } finally { + if (factory instanceof DisposableBean) { + ((DisposableBean) factory).destroy(); + } + } + } + + private static ValkeyConnectionFactory createConnectionFactory(String clientType) { + return switch (clientType.toLowerCase()) { + case "lettuce" -> new LettuceConnectionFactory(); + case "jedis" -> new JedisConnectionFactory(); + case "valkeyglide" -> new ValkeyGlideConnectionFactory(); + default -> throw new IllegalArgumentException("Unknown client: " + clientType); + }; + } + + private static void runPerformanceTest(StringValkeyTemplate template) { + // SET operations + long start = System.nanoTime(); + for (int i = 0; i < OPERATIONS; i++) { + template.opsForValue().set(KEY_PREFIX + i, "value" + i); + } + long setTime = System.nanoTime() - start; + printResult("SET", setTime); + + // GET operations + start = System.nanoTime(); + for (int i = 0; i < OPERATIONS; i++) { + template.opsForValue().get(KEY_PREFIX + i); + } + long getTime = System.nanoTime() - start; + printResult("GET", getTime); + + // DELETE operations + start = System.nanoTime(); + for (int i = 0; i < OPERATIONS; i++) { + template.delete(KEY_PREFIX + i); + } + long deleteTime = System.nanoTime() - start; + printResult("DELETE", deleteTime); + } + + private static void printResult(String operation, long durationNanos) { + long durationMs = durationNanos / 1_000_000; + System.out.printf("%s: %,d ops/sec (%.2f ms total)%n", + operation, (long) (OPERATIONS * 1000.0 / durationMs), durationMs / 1.0); + } +} diff --git a/performance/src/main/java/performance/ThreadedDirectClientPerformanceTest.java b/performance/src/main/java/performance/ThreadedDirectClientPerformanceTest.java new file mode 100644 index 00000000..fbb6c649 --- /dev/null +++ b/performance/src/main/java/performance/ThreadedDirectClientPerformanceTest.java @@ -0,0 +1,329 @@ +/* + * Copyright 2025 the original author or authors. + * + * 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 + * + * https://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 performance; + +import glide.api.GlideClient; +import glide.api.models.GlideString; +import glide.api.models.configuration.GlideClientConfiguration; +import glide.api.models.configuration.NodeAddress; +import io.lettuce.core.RedisClient; +import io.lettuce.core.RedisURI; +import io.lettuce.core.api.StatefulRedisConnection; +import io.lettuce.core.api.sync.RedisCommands; +import redis.clients.jedis.Jedis; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Multi-threaded direct client performance test across multiple threads. + */ +public class ThreadedDirectClientPerformanceTest { + + private static final int THREADS = 100; + private static final int OPERATIONS_PER_THREAD = 100; + private static final int TOTAL_OPERATIONS = THREADS * OPERATIONS_PER_THREAD; + private static final String KEY_PREFIX = "threaded:test:"; + + public static void main(String[] args) throws Exception { + String clientType = System.getProperty("client", "valkeyglide"); + + System.out.println("Running Multi-Threaded Direct Client Performance Test"); + System.out.println("Client: " + clientType); + System.out.println("Threads: " + THREADS); + System.out.println("Operations per thread: " + OPERATIONS_PER_THREAD); + System.out.println("Total operations: " + TOTAL_OPERATIONS); + System.out.println("----------------------------------------"); + + switch (clientType.toLowerCase()) { + case "valkeyglide" -> testValkeyGlide(); + case "lettuce" -> testLettuce(); + case "jedis" -> testJedis(); + default -> throw new IllegalArgumentException("Unknown client: " + clientType); + } + } + + private static void testValkeyGlide() throws Exception { + GlideClientConfiguration config = GlideClientConfiguration.builder() + .address(NodeAddress.builder().host("localhost").port(6379).build()) + .build(); + + try (GlideClient client = GlideClient.createClient(config).get()) { + ExecutorService executorService = Executors.newFixedThreadPool(THREADS); + AtomicInteger setSuccess = new AtomicInteger(0); + AtomicInteger getSuccess = new AtomicInteger(0); + AtomicInteger deleteSuccess = new AtomicInteger(0); + + try { + // SET operations + long start = System.nanoTime(); + Runnable setTask = () -> { + for (int i = 0; i < OPERATIONS_PER_THREAD; i++) { + try { + int id = Thread.currentThread().hashCode() * OPERATIONS_PER_THREAD + i; + client.set(GlideString.of(KEY_PREFIX + id), GlideString.of("value" + id)).get(); + setSuccess.incrementAndGet(); + } catch (Exception e) { + // Ignore + } + } + }; + + for (int i = 0; i < THREADS; i++) { + executorService.submit(setTask); + } + executorService.shutdown(); + executorService.awaitTermination(10, TimeUnit.SECONDS); + long setTime = System.nanoTime() - start; + printResult("SET", setTime, setSuccess.get()); + + // GET operations + executorService = Executors.newFixedThreadPool(THREADS); + start = System.nanoTime(); + Runnable getTask = () -> { + for (int i = 0; i < OPERATIONS_PER_THREAD; i++) { + try { + int id = Thread.currentThread().hashCode() * OPERATIONS_PER_THREAD + i; + client.get(GlideString.of(KEY_PREFIX + id)).get(); + getSuccess.incrementAndGet(); + } catch (Exception e) { + // Ignore + } + } + }; + + for (int i = 0; i < THREADS; i++) { + executorService.submit(getTask); + } + executorService.shutdown(); + executorService.awaitTermination(10, TimeUnit.SECONDS); + long getTime = System.nanoTime() - start; + printResult("GET", getTime, getSuccess.get()); + + // DELETE operations + executorService = Executors.newFixedThreadPool(THREADS); + start = System.nanoTime(); + Runnable deleteTask = () -> { + for (int i = 0; i < OPERATIONS_PER_THREAD; i++) { + try { + int id = Thread.currentThread().hashCode() * OPERATIONS_PER_THREAD + i; + client.del(new GlideString[]{GlideString.of(KEY_PREFIX + id)}).get(); + deleteSuccess.incrementAndGet(); + } catch (Exception e) { + // Ignore + } + } + }; + + for (int i = 0; i < THREADS; i++) { + executorService.submit(deleteTask); + } + executorService.shutdown(); + executorService.awaitTermination(10, TimeUnit.SECONDS); + long deleteTime = System.nanoTime() - start; + printResult("DELETE", deleteTime, deleteSuccess.get()); + } finally { + if (!executorService.isShutdown()) { + executorService.shutdown(); + } + } + } + } + + private static void testLettuce() throws Exception { + RedisClient client = RedisClient.create(RedisURI.create("redis://localhost:6379")); + + try (StatefulRedisConnection connection = client.connect()) { + RedisCommands commands = connection.sync(); + ExecutorService executorService = Executors.newFixedThreadPool(THREADS); + AtomicInteger setSuccess = new AtomicInteger(0); + AtomicInteger getSuccess = new AtomicInteger(0); + AtomicInteger deleteSuccess = new AtomicInteger(0); + + try { + // SET operations + long start = System.nanoTime(); + Runnable setTask = () -> { + for (int i = 0; i < OPERATIONS_PER_THREAD; i++) { + try { + int id = Thread.currentThread().hashCode() * OPERATIONS_PER_THREAD + i; + commands.set(KEY_PREFIX + id, "value" + id); + setSuccess.incrementAndGet(); + } catch (Exception e) { + // Ignore + } + } + }; + + for (int i = 0; i < THREADS; i++) { + executorService.submit(setTask); + } + executorService.shutdown(); + executorService.awaitTermination(10, TimeUnit.SECONDS); + long setTime = System.nanoTime() - start; + printResult("SET", setTime, setSuccess.get()); + + // GET operations + executorService = Executors.newFixedThreadPool(THREADS); + start = System.nanoTime(); + Runnable getTask = () -> { + for (int i = 0; i < OPERATIONS_PER_THREAD; i++) { + try { + int id = Thread.currentThread().hashCode() * OPERATIONS_PER_THREAD + i; + commands.get(KEY_PREFIX + id); + getSuccess.incrementAndGet(); + } catch (Exception e) { + // Ignore + } + } + }; + + for (int i = 0; i < THREADS; i++) { + executorService.submit(getTask); + } + executorService.shutdown(); + executorService.awaitTermination(10, TimeUnit.SECONDS); + long getTime = System.nanoTime() - start; + printResult("GET", getTime, getSuccess.get()); + + // DELETE operations + executorService = Executors.newFixedThreadPool(THREADS); + start = System.nanoTime(); + Runnable deleteTask = () -> { + for (int i = 0; i < OPERATIONS_PER_THREAD; i++) { + try { + int id = Thread.currentThread().hashCode() * OPERATIONS_PER_THREAD + i; + commands.del(KEY_PREFIX + id); + deleteSuccess.incrementAndGet(); + } catch (Exception e) { + // Ignore + } + } + }; + + for (int i = 0; i < THREADS; i++) { + executorService.submit(deleteTask); + } + executorService.shutdown(); + executorService.awaitTermination(10, TimeUnit.SECONDS); + long deleteTime = System.nanoTime() - start; + printResult("DELETE", deleteTime, deleteSuccess.get()); + } finally { + if (!executorService.isShutdown()) { + executorService.shutdown(); + } + } + } finally { + client.shutdown(); + } + } + + private static void testJedis() throws Exception { + // Jedis connections are not thread-safe, so we need one per thread + ExecutorService executorService = Executors.newFixedThreadPool(THREADS); + AtomicInteger setSuccess = new AtomicInteger(0); + AtomicInteger getSuccess = new AtomicInteger(0); + AtomicInteger deleteSuccess = new AtomicInteger(0); + + try { + // SET operations + long start = System.nanoTime(); + Runnable setTask = () -> { + try (Jedis jedis = new Jedis("localhost", 6379)) { + for (int i = 0; i < OPERATIONS_PER_THREAD; i++) { + try { + int id = Thread.currentThread().hashCode() * OPERATIONS_PER_THREAD + i; + jedis.set(KEY_PREFIX + id, "value" + id); + setSuccess.incrementAndGet(); + } catch (Exception e) { + // Ignore + } + } + } + }; + + for (int i = 0; i < THREADS; i++) { + executorService.submit(setTask); + } + executorService.shutdown(); + executorService.awaitTermination(10, TimeUnit.SECONDS); + long setTime = System.nanoTime() - start; + printResult("SET", setTime, setSuccess.get()); + + // GET operations + executorService = Executors.newFixedThreadPool(THREADS); + start = System.nanoTime(); + Runnable getTask = () -> { + try (Jedis jedis = new Jedis("localhost", 6379)) { + for (int i = 0; i < OPERATIONS_PER_THREAD; i++) { + try { + int id = Thread.currentThread().hashCode() * OPERATIONS_PER_THREAD + i; + jedis.get(KEY_PREFIX + id); + getSuccess.incrementAndGet(); + } catch (Exception e) { + // Ignore + } + } + } + }; + + for (int i = 0; i < THREADS; i++) { + executorService.submit(getTask); + } + executorService.shutdown(); + executorService.awaitTermination(10, TimeUnit.SECONDS); + long getTime = System.nanoTime() - start; + printResult("GET", getTime, getSuccess.get()); + + // DELETE operations + executorService = Executors.newFixedThreadPool(THREADS); + start = System.nanoTime(); + Runnable deleteTask = () -> { + try (Jedis jedis = new Jedis("localhost", 6379)) { + for (int i = 0; i < OPERATIONS_PER_THREAD; i++) { + try { + int id = Thread.currentThread().hashCode() * OPERATIONS_PER_THREAD + i; + jedis.del(KEY_PREFIX + id); + deleteSuccess.incrementAndGet(); + } catch (Exception e) { + // Ignore + } + } + } + }; + + for (int i = 0; i < THREADS; i++) { + executorService.submit(deleteTask); + } + executorService.shutdown(); + executorService.awaitTermination(10, TimeUnit.SECONDS); + long deleteTime = System.nanoTime() - start; + printResult("DELETE", deleteTime, deleteSuccess.get()); + } finally { + if (!executorService.isShutdown()) { + executorService.shutdown(); + } + } + } + + private static void printResult(String operation, long duration, int successful) { + System.out.printf("%s: %,d ops/sec (%.2f ms total), %.1f%% successful%n", + operation, (long) (TOTAL_OPERATIONS / (duration / 1_000_000_000.0)), duration / 1_000_000.0, + (successful * 100.0 / TOTAL_OPERATIONS)); + } +}