diff --git a/pom.xml b/pom.xml
index f5b8c6a..77d342d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -44,6 +44,19 @@
daily
+
+ sonatype-ossrh-snapshots
+ Sonatype OSSRH Snapshots
+ https://s01.oss.sonatype.org/content/repositories/snapshots/
+
+ false
+
+
+ true
+ always
+
+
+
@@ -97,6 +110,23 @@
com.fasterxml.jackson.datatype
jackson-datatype-jsr310
+
+
+
+ redis.clients
+ jedis
+ 7.0.0-SNAPSHOT
+
+
+ io.github.resilience4j
+ resilience4j-circuitbreaker
+ 1.7.1
+
+
+ io.github.resilience4j
+ resilience4j-retry
+ 1.7.1
+
@@ -121,10 +151,93 @@
-
- org.springframework.boot
- spring-boot-maven-plugin
-
+
+
-
\ No newline at end of file
+
+
+ lettuce
+
+ true
+
+
+ lettuce-test-app
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+ io.lettuce.test.LettuceTestApplication
+
+
+
+
+
+
+ jedis
+
+ jedis-test-app
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+ redis.clients.jedis.test.JedisTestApplication
+
+
+
+
+
+
+ all
+
+ lettuce-test-app
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+ io.lettuce.test.LettuceTestApplication
+
+
+
+ repackage-jedis
+
+ repackage
+
+
+ redis.clients.jedis.test.JedisTestApplication
+ jedis
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-antrun-plugin
+ 3.1.0
+
+
+ rename-artifacts
+ package
+
+
+
+
+
+
+
+ run
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/java/io/lettuce/test/config/TestRunProperties.java b/src/main/java/io/lettuce/test/config/TestRunProperties.java
index 78527f2..aedbb2c 100644
--- a/src/main/java/io/lettuce/test/config/TestRunProperties.java
+++ b/src/main/java/io/lettuce/test/config/TestRunProperties.java
@@ -24,9 +24,9 @@ public class TestRunProperties {
private String instanceId;
/**
- * Application name for identification purposes. Defaults to "lettuce-test-app".
+ * Application name for identification purposes. Defaults to spring.application.name.
*/
- @Value("${appName:lettuce-test-app}")
+ @Value("${appName:${spring.application.name}}")
private String appName;
public String getRunId() {
diff --git a/src/main/java/io/lettuce/test/config/WorkloadRunnerConfig.java b/src/main/java/io/lettuce/test/config/WorkloadRunnerConfig.java
index 7511836..c290458 100644
--- a/src/main/java/io/lettuce/test/config/WorkloadRunnerConfig.java
+++ b/src/main/java/io/lettuce/test/config/WorkloadRunnerConfig.java
@@ -11,7 +11,7 @@
@Configuration
@ConfigurationProperties(prefix = "runner")
-@PropertySource(value = "classpath:runner-config-defaults.yaml", factory = YamlPropertySourceFactory.class)
+@PropertySource(value = "classpath:${runner.defaults:runner-config-defaults.yaml}", factory = YamlPropertySourceFactory.class)
@PropertySource(value = "file:${runner.config:runner-config.yaml}", factory = YamlPropertySourceFactory.class, ignoreResourceNotFound = true)
public class WorkloadRunnerConfig {
diff --git a/src/main/java/io/lettuce/test/metrics/MetricsReporter.java b/src/main/java/io/lettuce/test/metrics/MetricsReporter.java
index 13a1353..fee18b2 100644
--- a/src/main/java/io/lettuce/test/metrics/MetricsReporter.java
+++ b/src/main/java/io/lettuce/test/metrics/MetricsReporter.java
@@ -134,7 +134,7 @@ public Timer.Sample startTimer() {
return Timer.start(meterRegistry);
}
- Timer.Sample startCommandTimer() {
+ public Timer.Sample startCommandTimer() {
return Timer.start(meterRegistry);
}
@@ -152,7 +152,7 @@ public record CommandKey(String commandName, OperationStatus status) {
}
- void recordCommandLatency(CommandKey commandKey, Timer.Sample sample) {
+ public void recordCommandLatency(CommandKey commandKey, Timer.Sample sample) {
Timer timer = commandLatencyTimers.computeIfAbsent(commandKey, this::createCommandLatencyTimer);
long timeNs = sample.stop(timer);
@@ -162,7 +162,7 @@ void recordCommandLatency(CommandKey commandKey, Timer.Sample sample) {
counter.increment();
}
- void incrementCommandError(String commandName) {
+ public void incrementCommandError(String commandName) {
commandErrorCounters.computeIfAbsent(commandName, this::createCommandErrorCounter).increment();
commandErrorTotalCounter.increment();
}
diff --git a/src/main/java/io/lettuce/test/workloads/BaseWorkload.java b/src/main/java/io/lettuce/test/workloads/BaseWorkload.java
index aa233c4..79c3b78 100644
--- a/src/main/java/io/lettuce/test/workloads/BaseWorkload.java
+++ b/src/main/java/io/lettuce/test/workloads/BaseWorkload.java
@@ -33,9 +33,9 @@ public enum Status {
protected MetricsReporter metricsReporter;
- private final CommonWorkloadOptions options;
+ protected final CommonWorkloadOptions options;
- private final KeyGenerator keyGenerator;
+ protected final KeyGenerator keyGenerator;
public BaseWorkload() {
options = DefaultWorkloadOptions.DEFAULT;
diff --git a/src/main/java/redis/clients/jedis/test/JedisRunner.java b/src/main/java/redis/clients/jedis/test/JedisRunner.java
new file mode 100644
index 0000000..9c7dff1
--- /dev/null
+++ b/src/main/java/redis/clients/jedis/test/JedisRunner.java
@@ -0,0 +1,7 @@
+package redis.clients.jedis.test;
+
+public interface JedisRunner extends AutoCloseable {
+
+ void run();
+
+}
diff --git a/src/main/java/redis/clients/jedis/test/JedisTestApplication.java b/src/main/java/redis/clients/jedis/test/JedisTestApplication.java
new file mode 100644
index 0000000..88a5ce0
--- /dev/null
+++ b/src/main/java/redis/clients/jedis/test/JedisTestApplication.java
@@ -0,0 +1,37 @@
+package redis.clients.jedis.test;
+
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import java.util.HashMap;
+import java.util.Map;
+
+@SpringBootApplication(scanBasePackages = { "io.lettuce.test.metrics", "io.lettuce.test.config", "redis.clients.jedis.test" })
+public class JedisTestApplication implements ApplicationRunner {
+
+ private final JedisWorkloadRunner runner;
+
+ public JedisTestApplication(JedisWorkloadRunner runner) {
+ this.runner = runner;
+ }
+
+ public static void main(String[] args) {
+ // Ensure Jedis-specific app name overrides application.properties
+ System.setProperty("spring.application.name", "jedis-test-app");
+ // Ensure appName property (used in metrics tags) aligns with the app name
+ System.setProperty("appName", "jedis-test-app");
+
+ SpringApplication app = new SpringApplication(JedisTestApplication.class);
+ Map defaults = new HashMap<>();
+ defaults.put("runner.defaults", "runner-config-defaults-jedis.yaml");
+ app.setDefaultProperties(defaults);
+ app.run(args);
+ }
+
+ @Override
+ public void run(ApplicationArguments args) {
+ runner.run();
+ }
+
+}
diff --git a/src/main/java/redis/clients/jedis/test/JedisWorkloadRunner.java b/src/main/java/redis/clients/jedis/test/JedisWorkloadRunner.java
new file mode 100644
index 0000000..f1dba51
--- /dev/null
+++ b/src/main/java/redis/clients/jedis/test/JedisWorkloadRunner.java
@@ -0,0 +1,50 @@
+package redis.clients.jedis.test;
+
+import io.lettuce.test.config.WorkloadRunnerConfig;
+import io.lettuce.test.metrics.MetricsReporter;
+import jakarta.annotation.PreDestroy;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+@Component
+public class JedisWorkloadRunner {
+
+ private static final Logger log = LoggerFactory.getLogger(JedisWorkloadRunner.class);
+
+ private final MetricsReporter metricsReporter;
+
+ private final WorkloadRunnerConfig config;
+
+ private JedisRunner runner;
+
+ public JedisWorkloadRunner(MetricsReporter metricsReporter, WorkloadRunnerConfig config) {
+ this.metricsReporter = metricsReporter;
+ this.config = config;
+ }
+
+ public void run() {
+ switch (config.getTest().getMode().toUpperCase()) {
+ case "STANDALONE" -> {
+ runner = new StandaloneJedisWorkloadRunner(config, metricsReporter);
+ log.info("Running standalone workload (Jedis/UnifiedJedis)");
+ }
+ default -> throw new IllegalArgumentException("Unsupported mode for Jedis app: " + config.getTest().getMode()
+ + ". Only STANDALONE is supported for Jedis right now.");
+ }
+ runner.run();
+ }
+
+ @PreDestroy
+ private void shutdown() {
+ log.info("Shutting down (Jedis app)...");
+ if (runner != null) {
+ try {
+ runner.close();
+ } catch (Exception e) {
+ log.warn("Error closing Jedis runner", e);
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/redis/clients/jedis/test/SchedulerConfig.java b/src/main/java/redis/clients/jedis/test/SchedulerConfig.java
new file mode 100644
index 0000000..dcc62e1
--- /dev/null
+++ b/src/main/java/redis/clients/jedis/test/SchedulerConfig.java
@@ -0,0 +1,24 @@
+package redis.clients.jedis.test;
+
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.TaskScheduler;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
+
+@Configuration
+@EnableScheduling
+public class SchedulerConfig {
+
+ @Bean
+ @Qualifier("metricReporterScheduler")
+ public TaskScheduler taskScheduler() {
+ ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
+ scheduler.setPoolSize(1); // Set pool size to 1 for a single thread
+ scheduler.setDaemon(true); // Make the thread a daemon thread
+ scheduler.setThreadNamePrefix("metrics-reporter-");
+ return scheduler;
+ }
+
+}
diff --git a/src/main/java/redis/clients/jedis/test/StandaloneJedisWorkloadRunner.java b/src/main/java/redis/clients/jedis/test/StandaloneJedisWorkloadRunner.java
new file mode 100644
index 0000000..106861b
--- /dev/null
+++ b/src/main/java/redis/clients/jedis/test/StandaloneJedisWorkloadRunner.java
@@ -0,0 +1,129 @@
+package redis.clients.jedis.test;
+
+import io.lettuce.test.CommonWorkloadOptions;
+import io.lettuce.test.DefaultWorkloadOptions;
+import io.lettuce.test.ContinuousWorkload;
+import io.lettuce.test.config.WorkloadRunnerConfig;
+import io.lettuce.test.config.WorkloadRunnerConfig.WorkloadConfig;
+import io.lettuce.test.metrics.MetricsReporter;
+import io.lettuce.test.workloads.BaseWorkload;
+import redis.clients.jedis.test.workloads.JedisRedisCommandsWorkload;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import redis.clients.jedis.DefaultJedisClientConfig;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.UnifiedJedis;
+import redis.clients.jedis.JedisClientConfig;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Standalone Jedis workload runner that mirrors the scheduling/execution behavior of the Lettuce runner.
+ */
+public class StandaloneJedisWorkloadRunner implements JedisRunner {
+
+ private static final Logger log = LoggerFactory.getLogger(StandaloneJedisWorkloadRunner.class);
+
+ private final WorkloadRunnerConfig config;
+
+ private final MetricsReporter metricsReporter;
+
+ private final ExecutorService executor = Executors.newCachedThreadPool();
+
+ private final List clients = new ArrayList<>();
+
+ public StandaloneJedisWorkloadRunner(WorkloadRunnerConfig config, MetricsReporter metricsReporter) {
+ this.config = Objects.requireNonNull(config);
+ this.metricsReporter = Objects.requireNonNull(metricsReporter);
+ }
+
+ public void run() {
+ List> connections = new ArrayList<>();
+
+ for (int i = 0; i < config.getTest().getClients(); i++) {
+ List clientConnections = new ArrayList<>();
+ for (int j = 0; j < config.getTest().getConnectionsPerClient(); j++) {
+ UnifiedJedis uj = createUnified(config.getRedis());
+ clients.add(uj);
+ clientConnections.add(uj);
+ }
+ connections.add(clientConnections);
+ }
+
+ metricsReporter.recordStartTime();
+ try {
+ CompletableFuture all = executeWorkloads(connections);
+ all.whenComplete((v, t) -> metricsReporter.recordEndTime())
+ .thenRun(() -> log.info("All Jedis tasks completed. Exiting..."));
+ } finally {
+ executor.shutdown();
+ }
+ }
+
+ private CompletableFuture executeWorkloads(List> connections) {
+ List> futures = new ArrayList<>();
+ WorkloadConfig workloadConfig = config.getTest().getWorkload();
+
+ for (int i = 0; i < connections.size(); i++) {
+ for (UnifiedJedis conn : connections.get(i)) {
+ for (int j = 0; j < config.getTest().getThreadsPerConnection(); j++) {
+ BaseWorkload workload = createWorkload(conn, workloadConfig);
+ workload.metricsReporter(metricsReporter);
+ futures.add(submit(workload, workloadConfig));
+ }
+ }
+ }
+
+ return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
+ }
+
+ private BaseWorkload createWorkload(UnifiedJedis conn, WorkloadConfig config) {
+ CommonWorkloadOptions options = DefaultWorkloadOptions.create(config.getOptions());
+ return switch (config.getType()) {
+ case "redis_commands" -> new JedisRedisCommandsWorkload(conn, options);
+ default -> throw new IllegalArgumentException(
+ "Invalid workload specified for Jedis standalone mode: " + config.getType());
+ };
+ }
+
+ // no-op helper retained for compatibility
+
+ private CompletableFuture submit(BaseWorkload task, WorkloadConfig config) {
+ ContinuousWorkload cw = new ContinuousWorkload(task, config);
+ return CompletableFuture.runAsync(cw::run, executor).thenApply(v -> cw);
+ }
+
+ private UnifiedJedis createUnified(WorkloadRunnerConfig.RedisConfig rc) {
+ JedisClientConfig clientCfg = DefaultJedisClientConfig.builder().user(rc.getUsername()).password(rc.getPassword())
+ .database(rc.getDatabase()).clientName(rc.getClientName()).ssl(rc.isUseTls())
+ .timeoutMillis(rc.getTimeout() != null ? (int) rc.getTimeout().toMillis() : 2000).build();
+ return new UnifiedJedis(new HostAndPort(rc.getHost(), rc.getPort()), clientCfg);
+ }
+
+ @Override
+ public void close() {
+ log.info("JedisWorkloadRunner stopping...");
+ executor.shutdown();
+ try {
+ if (!executor.awaitTermination(10, TimeUnit.SECONDS)) {
+ executor.shutdownNow();
+ }
+ } catch (InterruptedException e) {
+ executor.shutdownNow();
+ }
+ for (UnifiedJedis c : clients) {
+ try {
+ c.close();
+ } catch (Exception ignore) {
+ }
+ }
+ log.info("JedisWorkloadRunner stopped.");
+ }
+
+}
diff --git a/src/main/java/redis/clients/jedis/test/workloads/JedisBaseWorkload.java b/src/main/java/redis/clients/jedis/test/workloads/JedisBaseWorkload.java
new file mode 100644
index 0000000..73d547c
--- /dev/null
+++ b/src/main/java/redis/clients/jedis/test/workloads/JedisBaseWorkload.java
@@ -0,0 +1,56 @@
+package redis.clients.jedis.test.workloads;
+
+import io.lettuce.test.CommonWorkloadOptions;
+import io.lettuce.test.workloads.BaseWorkload;
+import io.micrometer.core.instrument.Timer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import redis.clients.jedis.UnifiedJedis;
+
+import java.util.function.Function;
+
+import static io.lettuce.test.metrics.MetricsReporter.cmdKeyError;
+import static io.lettuce.test.metrics.MetricsReporter.cmdKeyOk;
+
+public abstract class JedisBaseWorkload extends BaseWorkload {
+
+ protected static final Logger log = LoggerFactory.getLogger(JedisBaseWorkload.class);
+
+ protected final UnifiedJedis client;
+
+ public JedisBaseWorkload(UnifiedJedis jedis, CommonWorkloadOptions options) {
+ super(options);
+ this.client = jedis;
+ }
+
+ protected abstract void doRun();
+
+ protected abstract String getType();
+
+ @Override
+ public void run() {
+ Timer.Sample timer = metricsReporter.startTimer();
+
+ try {
+ doRun();
+ metricsReporter.recordWorkloadExecutionDuration(timer, getType(), BaseWorkload.Status.SUCCESSFUL);
+ } catch (Exception e) {
+ metricsReporter.recordWorkloadExecutionDuration(timer, getType(), BaseWorkload.Status.COMPLETED_WITH_ERRORS);
+ throw e;
+ }
+ }
+
+ protected R exec(String commandName, Function fn) {
+ Timer.Sample sample = metricsReporter.startCommandTimer();
+ try {
+ R result = fn.apply(client);
+ metricsReporter.recordCommandLatency(cmdKeyOk(commandName), sample);
+ return result;
+ } catch (RuntimeException ex) {
+ metricsReporter.incrementCommandError(commandName);
+ metricsReporter.recordCommandLatency(cmdKeyError(commandName, ex), sample);
+ }
+ return null;
+ }
+
+}
diff --git a/src/main/java/redis/clients/jedis/test/workloads/JedisRedisCommandsWorkload.java b/src/main/java/redis/clients/jedis/test/workloads/JedisRedisCommandsWorkload.java
new file mode 100644
index 0000000..eeb94b1
--- /dev/null
+++ b/src/main/java/redis/clients/jedis/test/workloads/JedisRedisCommandsWorkload.java
@@ -0,0 +1,51 @@
+package redis.clients.jedis.test.workloads;
+
+import io.lettuce.test.CommonWorkloadOptions;
+import io.lettuce.test.util.PayloadUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import redis.clients.jedis.UnifiedJedis;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class JedisRedisCommandsWorkload extends JedisBaseWorkload {
+
+ protected static final Logger log = LoggerFactory.getLogger(JedisRedisCommandsWorkload.class);
+
+ public JedisRedisCommandsWorkload(UnifiedJedis jedis, CommonWorkloadOptions options) {
+ super(jedis, options);
+ }
+
+ @Override
+ protected String getType() {
+ return "redis_commands";
+ }
+
+ @Override
+ protected void doRun() {
+ String payload = PayloadUtils.randomString(options().valueSize());
+
+ for (int i = 0; i < options().iterationCount(); i++) {
+ String key = keyGenerator().nextKey();
+
+ exec("set", c -> c.set(key, payload));
+ exec("get", c -> c.get(key));
+ exec("del", c -> c.del(key));
+ exec("incr", c -> c.incr("counter"));
+
+ List payloads = new ArrayList<>();
+ for (int j = 0; j < options().elementsCount(); j++) {
+ payloads.add(payload);
+ }
+ if (options().elementsCount() > 0) {
+ exec("lpush", c -> c.lpush(key + "list", payloads.toArray(new String[0])));
+ exec("lrange", c -> c.lrange(key + "list", 0, -1));
+ exec("ltrim", c -> c.ltrim(key + "list", 0, options().elementsCount()));
+ }
+
+ delay(options().delayAfterIteration());
+ }
+ }
+
+}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 640c484..0b69ccb 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -20,9 +20,9 @@ logging.metrics.step=PT5S
# logging
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} - %msg%n
logging.file.path=logs
-logging.file.name=${logging.file.path}/lettuce-test-app.log
+logging.file.name=${logging.file.path}/${spring.application.name}.log
logging.logback.rollingpolicy.max-file-size=50MB
-logging.file.metrics=${logging.file.path}/lettuce-test-app-metrics.log
+logging.file.metrics=${logging.file.path}/${spring.application.name}-metrics.log
#logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} - %msg%n
#logging.level.org.springframework=DEBUG
diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml
index 8851cdd..be88971 100644
--- a/src/main/resources/logback-spring.xml
+++ b/src/main/resources/logback-spring.xml
@@ -6,7 +6,7 @@
-
+
${CONFIG_LOG_FILE}
@@ -15,7 +15,7 @@
-
+
${METRICS_LOG_FILE}
@@ -24,7 +24,7 @@
-
+
${METRICS_TOTAL_LOG_FILE}