diff --git a/src/main/java/com/github/sttk/sabi_redis/RedisDataSrc.java b/src/main/java/com/github/sttk/sabi_redis/RedisDataSrc.java
index 283cea7..5efcf88 100644
--- a/src/main/java/com/github/sttk/sabi_redis/RedisDataSrc.java
+++ b/src/main/java/com/github/sttk/sabi_redis/RedisDataSrc.java
@@ -11,7 +11,6 @@
import io.lettuce.core.RedisClient;
import io.lettuce.core.RedisURI;
import io.lettuce.core.resource.ClientResources;
-import io.lettuce.core.resource.DefaultClientResources;
import java.net.URI;
/**
diff --git a/src/main/java/com/github/sttk/sabi_redis/package-info.java b/src/main/java/com/github/sttk/sabi_redis/package-info.java
index 1c5eab7..fddf8a7 100644
--- a/src/main/java/com/github/sttk/sabi_redis/package-info.java
+++ b/src/main/java/com/github/sttk/sabi_redis/package-info.java
@@ -9,7 +9,10 @@
* Provides classes to connect and operate Redis server for Sabi framework.
*
*
This package contains the {@code DataSrc} and {@code DataConn} classes which required to
- * connect to and operate Redis server in various configurations.
+ * connect to and operate Redis server in standalone configuration.
+ *
+ *
The sub-package {@code sentinel} contains the classes to connect and operate Redis server in
+ * sentinel configuration.
*
* @version 0.1
*/
diff --git a/src/main/java/com/github/sttk/sabi_redis/sentinel/RedisSentinelDataConn.java b/src/main/java/com/github/sttk/sabi_redis/sentinel/RedisSentinelDataConn.java
new file mode 100644
index 0000000..3500b9c
--- /dev/null
+++ b/src/main/java/com/github/sttk/sabi_redis/sentinel/RedisSentinelDataConn.java
@@ -0,0 +1,156 @@
+/*
+ * RedisSentinelDataConn.java
+ * Copyright (C) 2026 Takayuki Sato. All Rights Reserved.
+ */
+package com.github.sttk.sabi_redis.sentinel;
+
+import com.github.sttk.errs.Err;
+import com.github.sttk.sabi.AsyncGroup;
+import com.github.sttk.sabi.DataConn;
+import com.github.sttk.sabi_redis.ConnectionHandler;
+import io.lettuce.core.api.StatefulRedisConnection;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * The DataConn implementation for Redis in Sentinel configuration.
+ *
+ *
This class manages a connection to a Redis Sentinel and provides ways to register handlers
+ * that are executed at certain points in the connection's lifecycle.
+ */
+public class RedisSentinelDataConn implements DataConn {
+
+ // Fields
+
+ private final StatefulRedisConnection connection;
+ private final List preCommits = new ArrayList<>();
+ private final List postCommits = new ArrayList<>();
+ private final List forceBacks = new ArrayList<>();
+
+ // Constructors
+
+ /**
+ * Constructs a new RedisSentinelDataConn with the given Redis connection.
+ *
+ * @param connection a Redis connection.
+ */
+ protected RedisSentinelDataConn(StatefulRedisConnection connection) {
+ this.connection = connection;
+ }
+
+ // Methods
+
+ /**
+ * Returns the Redis connection.
+ *
+ * @return a Redis connection.
+ */
+ public StatefulRedisConnection getConnection() {
+ return this.connection;
+ }
+
+ /**
+ * Adds a handler to be executed before the connection is committed.
+ *
+ * @param handler a connection handler.
+ */
+ public void addPreCommit(ConnectionHandler handler) {
+ this.preCommits.add(handler);
+ }
+
+ /**
+ * Adds a handler to be executed after the connection is committed.
+ *
+ * @param handler a connection handler.
+ */
+ public void addPostCommit(ConnectionHandler handler) {
+ this.postCommits.add(handler);
+ }
+
+ /**
+ * Adds a handler to be executed when the connection is forced to roll back.
+ *
+ * @param handler a connection handler.
+ */
+ public void addForceBack(ConnectionHandler handler) {
+ this.forceBacks.add(handler);
+ }
+
+ /**
+ * Executes pre-commit handlers.
+ *
+ * @param ag an asynchronous group.
+ * @throws Err if an error occurs during pre-commit.
+ */
+ @Override
+ public void preCommit(AsyncGroup ag) throws Err {
+ for (var h : this.preCommits) {
+ h.handle(this.connection);
+ }
+ }
+
+ /**
+ * Commits the connection. (Currently does nothing as Redis doesn't have a direct commit for a
+ * simple connection)
+ *
+ * @param ag an asynchronous group.
+ * @throws Err if an error occurs during commit.
+ */
+ @Override
+ public void commit(AsyncGroup ag) throws Err {}
+
+ /**
+ * Executes post-commit handlers.
+ *
+ * @param ag an asynchronous group.
+ */
+ @Override
+ public void postCommit(AsyncGroup ag) {
+ for (var h : this.postCommits) {
+ try {
+ h.handle(this.connection);
+ } catch (Err e) {
+ } // Err will be thrown for error notification
+ }
+ }
+
+ /**
+ * Returns whether this connection should force back.
+ *
+ * @return true.
+ */
+ @Override
+ public boolean shouldForceBack() {
+ return true;
+ }
+
+ /**
+ * Rolls back the connection. (Currently does nothing as Redis doesn't have a direct rollback for
+ * a simple connection)
+ *
+ * @param ag an asynchronous group.
+ */
+ @Override
+ public void rollback(AsyncGroup ag) {}
+
+ /**
+ * Executes force-back handlers.
+ *
+ * @param ag an asynchronous group.
+ */
+ @Override
+ public void forceBack(AsyncGroup ag) {
+ for (var h : this.forceBacks) {
+ try {
+ h.handle(this.connection);
+ } catch (Err e) {
+ } // Err will be thrown for error notification
+ }
+ }
+
+ /** Closes the connection. */
+ @Override
+ public void close() {
+ this.connection.close();
+ }
+}
diff --git a/src/main/java/com/github/sttk/sabi_redis/sentinel/RedisSentinelDataSrc.java b/src/main/java/com/github/sttk/sabi_redis/sentinel/RedisSentinelDataSrc.java
new file mode 100644
index 0000000..6ed082b
--- /dev/null
+++ b/src/main/java/com/github/sttk/sabi_redis/sentinel/RedisSentinelDataSrc.java
@@ -0,0 +1,337 @@
+/*
+ * RedisSentinelDataSrc.java
+ * Copyright (C) 2026 Takayuki Sato. All Rights Reserved.
+ */
+package com.github.sttk.sabi_redis.sentinel;
+
+import com.github.sttk.errs.Err;
+import com.github.sttk.sabi.AsyncGroup;
+import com.github.sttk.sabi.DataConn;
+import com.github.sttk.sabi.DataSrc;
+import io.lettuce.core.RedisClient;
+import io.lettuce.core.RedisURI;
+import io.lettuce.core.resource.ClientResources;
+import java.net.URI;
+import java.util.Arrays;
+
+/**
+ * The DataSrc implementation for Redis in Sentinel configuration.
+ *
+ * This class is a factory for {@link RedisSentinelDataConn} and manages the lifecycle of the
+ * {@link RedisClient}.
+ */
+public class RedisSentinelDataSrc implements DataSrc {
+
+ // Error reasons
+
+ /** The error reason that indicates the {@link RedisSentinelDataSrc} is already setup. */
+ public record AlreadySetup() {}
+
+ /** The error reason that indicates the {@link RedisSentinelDataSrc} is not setup yet. */
+ public record NotSetupYet() {}
+
+ /**
+ * The error reason that indicates failing to create a {@link RedisURI} object from a URI string.
+ *
+ * @param uri a URI string.
+ * @param index an index of the URI string in the array.
+ */
+ public record FailToCreateRedisURI(String uri, int index) {}
+
+ /**
+ * The error reason that indicates failing to use a {@link RedisURI} object.
+ *
+ * @param redisURI a RedisURI object.
+ * @param index an index of the RedisURI object in the array.
+ */
+ public record BadRedisURI(RedisURI redisURI, int index) {}
+
+ /**
+ * The error reason that indicates the master ID is bad.
+ *
+ * @param masterId a master ID.
+ */
+ public record BadMasterId(String masterId) {}
+
+ /**
+ * The error reason that indicates failing to build a {@link RedisURI} object for Sentinel.
+ *
+ * @param builder a RedisURI builder.
+ */
+ public record FailToBuildSentinelRedisURI(RedisURI.Builder builder) {}
+
+ /**
+ * The error reason that indicates failing to create a {@link RedisClient} object.
+ *
+ * @param clientResources a ClientResources object.
+ * @param redisURI a RedisURI object.
+ */
+ public record FailToCreateClient(ClientResources clientResources, RedisURI redisURI) {}
+
+ /**
+ * The error reason that indicates failing to connect to a Redis Sentinel.
+ *
+ * @param clientResources a ClientResources object.
+ * @param redisURI a RedisURI object.
+ */
+ public record FailToConnectToRedis(ClientResources clientResources, RedisURI redisURI) {}
+
+ // Fields
+
+ private RedisClientFactory redisClientFactory;
+ private RedisClient redisClient;
+
+ // Constructors
+
+ /**
+ * Constructs a new RedisSentinelDataSrc with the given master ID and URI strings.
+ *
+ * @param masterId a master ID.
+ * @param uris URI strings.
+ */
+ public RedisSentinelDataSrc(String masterId, String... uris) {
+ this.redisClientFactory = new RedisClientFactoryWithUriStrings(null, masterId, uris);
+ }
+
+ /**
+ * Constructs a new RedisSentinelDataSrc with the given master ID and {@link URI} objects.
+ *
+ * @param masterId a master ID.
+ * @param uris URI objects.
+ */
+ public RedisSentinelDataSrc(String masterId, URI... uris) {
+ var uriStrings = Arrays.stream(uris).map(uri -> uri.toString()).toArray(String[]::new);
+ this.redisClientFactory = new RedisClientFactoryWithUriStrings(null, masterId, uriStrings);
+ }
+
+ /**
+ * Constructs a new RedisSentinelDataSrc with the given master ID and {@link RedisURI} objects.
+ *
+ * @param masterId a master ID.
+ * @param redisURIs RedisURI objects.
+ */
+ public RedisSentinelDataSrc(String masterId, RedisURI... redisURIs) {
+ this.redisClientFactory = new RedisClientFactoryWithRedisURIs(null, masterId, redisURIs);
+ }
+
+ /**
+ * Constructs a new RedisSentinelDataSrc with the given {@link ClientResources} object, master ID,
+ * and URI strings.
+ *
+ * @param cr a ClientResources object.
+ * @param masterId a master ID.
+ * @param uris URI strings.
+ */
+ public RedisSentinelDataSrc(ClientResources cr, String masterId, String... uris) {
+ this.redisClientFactory = new RedisClientFactoryWithUriStrings(cr, masterId, uris);
+ }
+
+ /**
+ * Constructs a new RedisSentinelDataSrc with the given {@link ClientResources} object, master ID,
+ * and {@link URI} objects.
+ *
+ * @param cr a ClientResources object.
+ * @param masterId a master ID.
+ * @param uris URI objects.
+ */
+ public RedisSentinelDataSrc(ClientResources cr, String masterId, URI... uris) {
+ var uriStrings = Arrays.stream(uris).map(uri -> uri.toString()).toArray(String[]::new);
+ this.redisClientFactory = new RedisClientFactoryWithUriStrings(cr, masterId, uriStrings);
+ }
+
+ /**
+ * Constructs a new RedisSentinelDataSrc with the given {@link ClientResources} object, master ID,
+ * and {@link RedisURI} objects.
+ *
+ * @param cr a ClientResources object.
+ * @param masterId a master ID.
+ * @param redisURIs RedisURI objects.
+ */
+ public RedisSentinelDataSrc(ClientResources cr, String masterId, RedisURI... redisURIs) {
+ this.redisClientFactory = new RedisClientFactoryWithRedisURIs(cr, masterId, redisURIs);
+ }
+
+ // Methods
+
+ /**
+ * Sets up this data source.
+ *
+ *
This method creates a {@link RedisClient} based on the parameters passed to the constructor.
+ *
+ * @param ag an asynchronous group.
+ * @throws Err if an error occurs during setup.
+ */
+ @Override
+ public void setup(AsyncGroup ag) throws Err {
+ if (this.redisClientFactory == null || this.redisClient != null) {
+ throw new Err(new AlreadySetup());
+ }
+ var factory = this.redisClientFactory;
+ this.redisClientFactory = null;
+ this.redisClient = factory.create();
+ }
+
+ /** Closes this data source and shuts down the {@link RedisClient}. */
+ @Override
+ public void close() {
+ if (this.redisClient != null) {
+ var redisClient = this.redisClient;
+ this.redisClient = null;
+ redisClient.shutdown();
+ }
+ }
+
+ /**
+ * Creates a new {@link DataConn} for Redis Sentinel.
+ *
+ * @return a new RedisSentinelDataConn.
+ * @throws Err if the data source is not setup yet.
+ */
+ @Override
+ public DataConn createDataConn() throws Err {
+ if (this.redisClient == null) {
+ throw new Err(new NotSetupYet());
+ }
+ return new RedisSentinelDataConn(this.redisClient.connect());
+ }
+
+ // Inner classes
+
+ private interface RedisClientFactory {
+ RedisClient create() throws Err;
+ }
+
+ private class RedisClientFactoryWithUriStrings implements RedisClientFactory {
+ final ClientResources cr;
+ final String masterId;
+ final String[] uris;
+
+ RedisClientFactoryWithUriStrings(ClientResources cr, String masterId, String[] uris) {
+ this.cr = cr;
+ this.masterId = masterId;
+ this.uris = uris;
+ }
+
+ @Override
+ @SuppressWarnings("try")
+ public RedisClient create() throws Err {
+ RedisURI.Builder builder = null;
+ if (this.uris.length == 0) {
+ builder = RedisURI.builder();
+ } else {
+ RedisURI ru0 = null;
+ try {
+ ru0 = RedisURI.create(this.uris[0]);
+ } catch (Exception e) {
+ throw new Err(new FailToCreateRedisURI(this.uris[0], 0), e);
+ }
+ builder = RedisURI.builder(ru0);
+ for (int i = 1; i < this.uris.length; i++) {
+ RedisURI ru = null;
+ try {
+ ru = RedisURI.create(this.uris[i]);
+ } catch (Exception e) {
+ throw new Err(new FailToCreateRedisURI(this.uris[i], i), e);
+ }
+ builder.withSentinel(ru);
+ }
+ }
+ try {
+ builder.withSentinelMasterId(this.masterId);
+ } catch (Exception e) {
+ throw new Err(new BadMasterId(this.masterId), e);
+ }
+
+ RedisURI redisURI = null;
+ try {
+ redisURI = builder.build();
+ } catch (Exception e) {
+ throw new Err(new FailToBuildSentinelRedisURI(builder), e);
+ }
+
+ RedisClient client = null;
+ try {
+ if (this.cr == null) {
+ client = RedisClient.create(redisURI);
+ } else {
+ client = RedisClient.create(this.cr, redisURI);
+ }
+ } catch (Exception e) {
+ throw new Err(new FailToCreateClient(this.cr, redisURI), e);
+ }
+
+ try (var conn = client.connect()) {
+ } catch (Exception e) {
+ client.shutdown();
+ throw new Err(new FailToConnectToRedis(this.cr, redisURI), e);
+ }
+
+ return client;
+ }
+ }
+
+ private class RedisClientFactoryWithRedisURIs implements RedisClientFactory {
+ final ClientResources cr;
+ final String masterId;
+ final RedisURI[] redisURIs;
+
+ RedisClientFactoryWithRedisURIs(ClientResources cr, String masterId, RedisURI[] redisURIs) {
+ this.cr = cr;
+ this.masterId = masterId;
+ this.redisURIs = redisURIs;
+ }
+
+ @Override
+ @SuppressWarnings("try")
+ public RedisClient create() throws Err {
+ RedisURI.Builder builder = null;
+ if (this.redisURIs.length == 0) {
+ builder = RedisURI.builder();
+ } else {
+ try {
+ builder = RedisURI.builder(this.redisURIs[0]);
+ } catch (Exception e) {
+ throw new Err(new BadRedisURI(this.redisURIs[0], 0), e);
+ }
+ for (int i = 1; i < this.redisURIs.length; i++) {
+ try {
+ builder.withSentinel(this.redisURIs[i]);
+ } catch (Exception e) {
+ throw new Err(new BadRedisURI(this.redisURIs[i], i), e);
+ }
+ }
+ }
+ try {
+ builder.withSentinelMasterId(this.masterId);
+ } catch (Exception e) {
+ throw new Err(new BadMasterId(this.masterId), e);
+ }
+
+ RedisURI redisURI = null;
+ try {
+ redisURI = builder.build();
+ } catch (Exception e) {
+ throw new Err(new FailToBuildSentinelRedisURI(builder), e);
+ }
+
+ RedisClient client = null;
+ try {
+ if (this.cr == null) {
+ client = RedisClient.create(redisURI);
+ } else {
+ client = RedisClient.create(this.cr, redisURI);
+ }
+ } catch (Exception e) {
+ throw new Err(new FailToCreateClient(this.cr, redisURI), e);
+ }
+
+ try (var conn = client.connect()) {
+ } catch (Exception e) {
+ client.shutdown();
+ throw new Err(new FailToConnectToRedis(this.cr, redisURI), e);
+ }
+
+ return client;
+ }
+ }
+}
diff --git a/src/main/java/com/github/sttk/sabi_redis/sentinel/package-info.java b/src/main/java/com/github/sttk/sabi_redis/sentinel/package-info.java
new file mode 100644
index 0000000..2b8c496
--- /dev/null
+++ b/src/main/java/com/github/sttk/sabi_redis/sentinel/package-info.java
@@ -0,0 +1,9 @@
+/*
+ * Copyright (C) 2026 Takayuki Sato. All Rights Reserved.
+ *
+ * This program is free software under MIT License. See the file LICENSE in this distribution for
+ * more details.
+ */
+
+/** This package provides the sabi-redis implementation for Redis Sentinel. */
+package com.github.sttk.sabi_redis.sentinel;
diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java
index f01ecfb..bfee637 100644
--- a/src/main/java/module-info.java
+++ b/src/main/java/module-info.java
@@ -13,6 +13,7 @@
*/
module com.github.sttk.sabi_redis {
exports com.github.sttk.sabi_redis;
+ exports com.github.sttk.sabi_redis.sentinel;
requires transitive com.github.sttk.sabi;
requires transitive com.github.sttk.errs;
diff --git a/src/test/java/com/github/sttk/sabi_redis/StandaloneTest.java b/src/test/java/com/github/sttk/sabi_redis/StandaloneTest.java
index 84fe006..647ee12 100644
--- a/src/test/java/com/github/sttk/sabi_redis/StandaloneTest.java
+++ b/src/test/java/com/github/sttk/sabi_redis/StandaloneTest.java
@@ -358,7 +358,7 @@ void test_NewRedisDataSrcWithClientResourcesAndRedisURI() {
}
@Test
- void test_NewRedisDataSrcWithClientResourcesAndRedisURIButInvalidAddr() {
+ void test_NewRedisDataSrcWithClientResourcesAndRedisURIButNotFoundAddr() {
var cr = DefaultClientResources.create();
try (var data = new DataHub()) {
data.uses("redis", new RedisDataSrc(cr, RedisURI.create("redis://127.0.0.1:9999/1")));
diff --git a/src/test/java/com/github/sttk/sabi_redis/sentinel/SentinelAsyncTest.java b/src/test/java/com/github/sttk/sabi_redis/sentinel/SentinelAsyncTest.java
new file mode 100644
index 0000000..5e181da
--- /dev/null
+++ b/src/test/java/com/github/sttk/sabi_redis/sentinel/SentinelAsyncTest.java
@@ -0,0 +1,337 @@
+package com.github.sttk.sabi_redis.sentinel;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.fail;
+
+import com.github.sttk.errs.Err;
+import com.github.sttk.sabi.DataAcc;
+import com.github.sttk.sabi.DataHub;
+import com.github.sttk.sabi.Logic;
+import io.lettuce.core.RedisClient;
+import io.lettuce.core.RedisURI;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Future;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable;
+
+@DisabledIfEnvironmentVariable(named = "CI", matches = ".*")
+public class SentinelAsyncTest {
+ private SentinelAsyncTest() {}
+
+ record FailToGetValue() {}
+
+ record FailToSetValue() {}
+
+ record FailToDelValue() {}
+
+ interface RedisSampleDataAcc extends DataAcc, SampleData {
+ default Future getSampleKey() throws Err {
+ var dc = getDataConn("redis", RedisSentinelDataConn.class);
+ var redisConn = dc.getConnection();
+ var commands = redisConn.async();
+ try {
+ return commands.get("sample/sentinel/async");
+ } catch (Exception e) {
+ throw new Err(new FailToGetValue(), e);
+ }
+ }
+
+ default Future setSampleKey(String val) throws Err {
+ var dc = getDataConn("redis", RedisSentinelDataConn.class);
+ var redisConn = dc.getConnection();
+ var commands = redisConn.async();
+ try {
+ return commands.set("sample/sentinel/async", val);
+ } catch (Exception e) {
+ throw new Err(new FailToSetValue(), e);
+ }
+ }
+
+ default Future delSampleKey() throws Err {
+ var dc = getDataConn("redis", RedisSentinelDataConn.class);
+ var redisConn = dc.getConnection();
+ var commands = redisConn.async();
+ try {
+ return commands.del("sample/sentinel/async");
+ } catch (Exception e) {
+ throw new Err(new FailToDelValue(), e);
+ }
+ }
+
+ default List> setSampleKeyWithForceBack(String val) throws Err {
+ var dc = getDataConn("redis", RedisSentinelDataConn.class);
+ var redisConn = dc.getConnection();
+ var commands = redisConn.async();
+
+ var futureList = new ArrayList>();
+
+ try {
+ futureList.add(commands.set("sample_force_back/sentinel/async", val));
+ } catch (Exception e) {
+ throw new Err(new FailToSetValue(), e);
+ }
+
+ dc.addForceBack(
+ redisConn1 -> {
+ var commands1 = redisConn1.async();
+ try {
+ var future = commands1.del("sample_force_back/sentinel/async");
+ future.get();
+ } catch (Exception e) {
+ throw new Err("fail to force back", e);
+ }
+ });
+
+ try {
+ futureList.add(commands.set("sample_force_back_2/sentinel/async", val));
+ } catch (Exception e) {
+ throw new Err(new FailToSetValue(), e);
+ }
+
+ dc.addForceBack(
+ redisConn1 -> {
+ var commands1 = redisConn1.async();
+ try {
+ var future = commands1.del("sample_force_back_2/sentinel/async");
+ future.get();
+ } catch (Exception e) {
+ throw new Err("fail to force back", e);
+ }
+ });
+
+ return futureList;
+ }
+
+ default void setSampleKeyWithPreCommit(String val) throws Err {
+ var dc = getDataConn("redis", RedisSentinelDataConn.class);
+ var redisConn = dc.getConnection();
+
+ dc.addPreCommit(
+ redisConn1 -> {
+ var commands1 = redisConn1.async();
+ var future = commands1.set("sample_pre_commit/sentinel/async", val);
+ try {
+ future.get();
+ } catch (Exception e) {
+ fail(e);
+ }
+ });
+ }
+
+ default void setSampleKeyWithPostCommit(String val) throws Err {
+ var dc = getDataConn("redis", RedisSentinelDataConn.class);
+ var redisConn = dc.getConnection();
+
+ dc.addPreCommit(
+ redisConn1 -> {
+ var commands1 = redisConn1.async();
+ var future = commands1.set("sample_post_commit/sentinel/async", val);
+ try {
+ future.get();
+ } catch (Exception e) {
+ fail(e);
+ }
+ });
+ }
+ }
+
+ interface SampleData {
+ Future getSampleKey() throws Err;
+
+ Future setSampleKey(String val) throws Err;
+
+ Future delSampleKey() throws Err;
+
+ List> setSampleKeyWithForceBack(String val) throws Err;
+
+ void setSampleKeyWithPreCommit(String val) throws Err;
+
+ void setSampleKeyWithPostCommit(String val) throws Err;
+ }
+
+ Logic sampleLogic =
+ (SampleData data) -> {
+ try {
+ var f = data.getSampleKey();
+ assertThat(f.get()).isNull();
+
+ data.setSampleKey("Hello").get();
+
+ var fut = data.getSampleKey();
+ assertThat(fut.get()).isEqualTo("Hello");
+
+ data.delSampleKey().get();
+ } catch (Exception e) {
+ fail(e);
+ }
+ };
+
+ Logic sampleLogicWithForceBackOk =
+ (SampleData data) -> {
+ try {
+ var futureList = data.setSampleKeyWithForceBack("Good Morning");
+ for (var f : futureList) {
+ f.get();
+ }
+ } catch (Exception e) {
+ fail(e);
+ }
+ };
+
+ Logic sampleLogicWithForceBackErr =
+ (SampleData data) -> {
+ try {
+ var futureList = data.setSampleKeyWithForceBack("Good Afternoon");
+ for (var f : futureList) {
+ f.get();
+ }
+ } catch (Exception e) {
+ fail(e);
+ }
+ throw new Err("XXX");
+ };
+
+ Logic sampleLogicWithPreCommit =
+ (SampleData data) -> {
+ data.setSampleKeyWithPreCommit("Good Evening");
+ };
+
+ Logic sampleLogicWithPostCommit =
+ (SampleData data) -> {
+ data.setSampleKeyWithPostCommit("Good Night");
+ };
+
+ class SampleDataHub extends DataHub implements SampleData, RedisSampleDataAcc {}
+
+ //
+
+ @Test
+ void test_TxnAndForceBack() {
+ try (var data = new SampleDataHub()) {
+ data.uses(
+ "redis",
+ new RedisSentinelDataSrc(
+ "mymaster",
+ "redis://127.0.0.1:26479",
+ "redis://127.0.0.1:26480",
+ "redis://127.0.0.1:26481"));
+ try {
+ data.txn(sampleLogicWithForceBackOk);
+ } catch (Err err) {
+ fail(err);
+ }
+
+ {
+ var client =
+ RedisClient.create(
+ RedisURI.Builder.sentinel("127.0.0.1", 26479, "mymaster")
+ .withSentinel("redis://127.0.0.1:26480")
+ .withSentinel("redis://127.0.0.1:26481")
+ .build());
+ var conn = client.connect();
+ var cmd = conn.sync();
+
+ var s = cmd.get("sample_force_back/sentinel/async");
+ cmd.del("sample_force_back/sentinel/async");
+ assertThat(s).isEqualTo("Good Morning");
+
+ s = cmd.get("sample_force_back_2/sentinel/async");
+ cmd.del("sample_force_back_2/sentinel/async");
+ assertThat(s).isEqualTo("Good Morning");
+ }
+
+ try {
+ data.txn(sampleLogicWithForceBackErr);
+ fail();
+ } catch (Err err) {
+ assertThat(err.getReason()).isEqualTo("XXX");
+ }
+
+ {
+ var client =
+ RedisClient.create(
+ RedisURI.Builder.sentinel("127.0.0.1", 26479, "mymaster")
+ .withSentinel("redis://127.0.0.1:26480")
+ .withSentinel("redis://127.0.0.1:26481")
+ .build());
+ var conn = client.connect();
+ var cmd = conn.sync();
+
+ var s = cmd.get("sample_force_back/sentinel/async");
+ cmd.del("sample_force_back/sentinel/async");
+ assertThat(s).isNull();
+
+ s = cmd.get("sample_force_back_2/sentinel/async");
+ cmd.del("sample_force_back_2/sentinel/async");
+ assertThat(s).isNull();
+ }
+ }
+ }
+
+ @Test
+ void test_TxnAndPreCommit() {
+ try (var data = new SampleDataHub()) {
+ data.uses(
+ "redis",
+ new RedisSentinelDataSrc(
+ "mymaster",
+ "redis://127.0.0.1:26479",
+ "redis://127.0.0.1:26480",
+ "redis://127.0.0.1:26481"));
+ try {
+ data.txn(sampleLogicWithPreCommit);
+ } catch (Err err) {
+ fail(err);
+ }
+
+ {
+ var client =
+ RedisClient.create(
+ RedisURI.Builder.sentinel("127.0.0.1", 26479, "mymaster")
+ .withSentinel("redis://127.0.0.1:26480")
+ .withSentinel("redis://127.0.0.1:26481")
+ .build());
+ var conn = client.connect();
+ var cmd = conn.sync();
+
+ var s = cmd.get("sample_pre_commit/sentinel/async");
+ cmd.del("sample_pre_commit/sentinel/async");
+ assertThat(s).isEqualTo("Good Evening");
+ }
+ }
+ }
+
+ @Test
+ void test_TxnAndPostCommit() {
+ try (var data = new SampleDataHub()) {
+ data.uses(
+ "redis",
+ new RedisSentinelDataSrc(
+ "mymaster",
+ "redis://127.0.0.1:26479",
+ "redis://127.0.0.1:26480",
+ "redis://127.0.0.1:26481"));
+ try {
+ data.txn(sampleLogicWithPostCommit);
+ } catch (Err err) {
+ fail(err);
+ }
+
+ {
+ var client =
+ RedisClient.create(
+ RedisURI.Builder.sentinel("127.0.0.1", 26479, "mymaster")
+ .withSentinel("redis://127.0.0.1:26480")
+ .withSentinel("redis://127.0.0.1:26481")
+ .build());
+ var conn = client.connect();
+ var cmd = conn.sync();
+
+ var s = cmd.get("sample_post_commit/sentinel/async");
+ cmd.del("sample_post_commit/sentinel/async");
+ assertThat(s).isEqualTo("Good Night");
+ }
+ }
+ }
+}
diff --git a/src/test/java/com/github/sttk/sabi_redis/sentinel/SentinelSyncTest.java b/src/test/java/com/github/sttk/sabi_redis/sentinel/SentinelSyncTest.java
new file mode 100644
index 0000000..f9f4dfc
--- /dev/null
+++ b/src/test/java/com/github/sttk/sabi_redis/sentinel/SentinelSyncTest.java
@@ -0,0 +1,300 @@
+package com.github.sttk.sabi_redis.sentinel;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.fail;
+
+import com.github.sttk.errs.Err;
+import com.github.sttk.sabi.DataAcc;
+import com.github.sttk.sabi.DataHub;
+import com.github.sttk.sabi.Logic;
+import io.lettuce.core.RedisClient;
+import io.lettuce.core.RedisURI;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable;
+
+@DisabledIfEnvironmentVariable(named = "CI", matches = ".*")
+public class SentinelSyncTest {
+ private SentinelSyncTest() {}
+
+ record FailToGetValue() {}
+
+ record FailToSetValue() {}
+
+ record FailToDelValue() {}
+
+ interface RedisSampleDataAcc extends DataAcc, SampleData {
+ default String getSampleKey() throws Err {
+ var dc = getDataConn("redis", RedisSentinelDataConn.class);
+ var redisConn = dc.getConnection();
+ var commands = redisConn.sync();
+ try {
+ return commands.get("sample/sentinel");
+ } catch (Exception e) {
+ throw new Err(new FailToGetValue(), e);
+ }
+ }
+
+ default void setSampleKey(String val) throws Err {
+ var dc = getDataConn("redis", RedisSentinelDataConn.class);
+ var redisConn = dc.getConnection();
+ var commands = redisConn.sync();
+ try {
+ commands.set("sample/sentinel", val);
+ } catch (Exception e) {
+ throw new Err(new FailToSetValue(), e);
+ }
+ }
+
+ default void delSampleKey() throws Err {
+ var dc = getDataConn("redis", RedisSentinelDataConn.class);
+ var redisConn = dc.getConnection();
+ var commands = redisConn.sync();
+ try {
+ commands.del("sample/sentinel");
+ } catch (Exception e) {
+ throw new Err(new FailToDelValue(), e);
+ }
+ }
+
+ default void setSampleKeyWithForceBack(String val) throws Err {
+ var dc = getDataConn("redis", RedisSentinelDataConn.class);
+ var redisConn = dc.getConnection();
+ var commands = redisConn.sync();
+
+ try {
+ commands.set("sample_force_back/sentinel", val);
+ } catch (Exception e) {
+ throw new Err(new FailToSetValue(), e);
+ }
+
+ dc.addForceBack(
+ redisConn1 -> {
+ var commands1 = redisConn1.sync();
+ try {
+ commands1.del("sample_force_back/sentinel");
+ } catch (Exception e) {
+ throw new Err("fail to force back", e);
+ }
+ });
+
+ try {
+ commands.set("sample_force_back_2/sentinel", val);
+ } catch (Exception e) {
+ throw new Err(new FailToSetValue(), e);
+ }
+
+ dc.addForceBack(
+ redisConn1 -> {
+ var commands1 = redisConn1.sync();
+ try {
+ commands1.del("sample_force_back_2/sentinel");
+ } catch (Exception e) {
+ throw new Err("fail to force back", e);
+ }
+ });
+ }
+
+ default void setSampleKeyWithPreCommit(String val) throws Err {
+ var dc = getDataConn("redis", RedisSentinelDataConn.class);
+ var redisConn = dc.getConnection();
+
+ dc.addPreCommit(
+ redisConn1 -> {
+ var commands1 = redisConn1.sync();
+ commands1.set("sample_pre_commit/sentinel", val);
+ });
+ }
+
+ default void setSampleKeyWithPostCommit(String val) throws Err {
+ var dc = getDataConn("redis", RedisSentinelDataConn.class);
+ var redisConn = dc.getConnection();
+
+ dc.addPostCommit(
+ redisConn1 -> {
+ var commands1 = redisConn1.sync();
+ commands1.set("sample_post_commit/sentinel", val);
+ });
+ }
+ }
+
+ interface SampleData {
+ String getSampleKey() throws Err;
+
+ void setSampleKey(String val) throws Err;
+
+ void delSampleKey() throws Err;
+
+ void setSampleKeyWithForceBack(String val) throws Err;
+
+ void setSampleKeyWithPreCommit(String val) throws Err;
+
+ void setSampleKeyWithPostCommit(String val) throws Err;
+ }
+
+ Logic sampleLogic =
+ (SampleData data) -> {
+ var val = data.getSampleKey();
+ assertThat(val).isNull();
+
+ data.setSampleKey("Hello");
+
+ val = data.getSampleKey();
+ assertThat(val).isEqualTo("Hello");
+
+ data.delSampleKey();
+ };
+
+ Logic sampleLogicWithForceBackOk =
+ (SampleData data) -> {
+ data.setSampleKeyWithForceBack("Good Morning");
+ };
+
+ Logic sampleLogicWithForceBackErr =
+ (SampleData data) -> {
+ data.setSampleKeyWithForceBack("Good Afternoon");
+ throw new Err("XXX");
+ };
+
+ Logic sampleLogicWithPreCommit =
+ (SampleData data) -> {
+ data.setSampleKeyWithPreCommit("Good Evening");
+ };
+
+ Logic sampleLogicWithPostCommit =
+ (SampleData data) -> {
+ data.setSampleKeyWithPostCommit("Good Night");
+ };
+
+ class SampleDataHub extends DataHub implements SampleData, RedisSampleDataAcc {}
+
+ //
+
+ @Test
+ void test_TxnAndForceBack() {
+ try (var data = new SampleDataHub()) {
+ data.uses(
+ "redis",
+ new RedisSentinelDataSrc(
+ "mymaster",
+ "redis://127.0.0.1:26479",
+ "redis://127.0.0.1:26480",
+ "redis://127.0.0.1:26481"));
+ try {
+ data.txn(sampleLogicWithForceBackOk);
+ } catch (Err err) {
+ fail(err);
+ }
+
+ {
+ var client =
+ RedisClient.create(
+ RedisURI.Builder.sentinel("127.0.0.1", 26479, "mymaster")
+ .withSentinel("redis://127.0.0.1:26480")
+ .withSentinel("redis://127.0.0.1:26481")
+ .build());
+ var conn = client.connect();
+ var cmd = conn.sync();
+
+ var s = cmd.get("sample_force_back/sentinel");
+ cmd.del("sample_force_back/sentinel");
+ assertThat(s).isEqualTo("Good Morning");
+
+ s = cmd.get("sample_force_back_2/sentinel");
+ cmd.del("sample_force_back_2/sentinel");
+ assertThat(s).isEqualTo("Good Morning");
+ }
+
+ try {
+ data.txn(sampleLogicWithForceBackErr);
+ fail();
+ } catch (Err err) {
+ assertThat(err.getReason()).isEqualTo("XXX");
+ }
+
+ {
+ var client =
+ RedisClient.create(
+ RedisURI.Builder.sentinel("127.0.0.1", 26479, "mymaster")
+ .withSentinel("redis://127.0.0.1:26480")
+ .withSentinel("redis://127.0.0.1:26481")
+ .build());
+ var conn = client.connect();
+ var cmd = conn.sync();
+
+ var s = cmd.get("sample_force_back/sentinel");
+ cmd.del("sample_force_back/sentinel");
+ assertThat(s).isNull();
+
+ s = cmd.get("sample_force_back_2/sentinel");
+ cmd.del("sample_force_back_2/sentinel");
+ assertThat(s).isNull();
+ }
+ }
+ }
+
+ @Test
+ void test_TxnAndPreCommit() {
+ try (var data = new SampleDataHub()) {
+ data.uses(
+ "redis",
+ new RedisSentinelDataSrc(
+ "mymaster",
+ "redis://127.0.0.1:26479",
+ "redis://127.0.0.1:26480",
+ "redis://127.0.0.1:26481"));
+ try {
+ data.txn(sampleLogicWithPreCommit);
+ } catch (Err err) {
+ fail(err);
+ }
+
+ {
+ var client =
+ RedisClient.create(
+ RedisURI.Builder.sentinel("127.0.0.1", 26479, "mymaster")
+ .withSentinel("redis://127.0.0.1:26480")
+ .withSentinel("redis://127.0.0.1:26481")
+ .build());
+ var conn = client.connect();
+ var cmd = conn.sync();
+
+ var s = cmd.get("sample_pre_commit/sentinel");
+ cmd.del("sample_pre_commit/sentinel");
+ assertThat(s).isEqualTo("Good Evening");
+ }
+ }
+ }
+
+ @Test
+ void test_TxnAndPostCommit() {
+ try (var data = new SampleDataHub()) {
+ data.uses(
+ "redis",
+ new RedisSentinelDataSrc(
+ "mymaster",
+ "redis://127.0.0.1:26479",
+ "redis://127.0.0.1:26480",
+ "redis://127.0.0.1:26481"));
+ try {
+ data.txn(sampleLogicWithPostCommit);
+ } catch (Err err) {
+ fail(err);
+ }
+
+ {
+ var client =
+ RedisClient.create(
+ RedisURI.Builder.sentinel("127.0.0.1", 26479, "mymaster")
+ .withSentinel("redis://127.0.0.1:26480")
+ .withSentinel("redis://127.0.0.1:26481")
+ .build());
+ var conn = client.connect();
+ var cmd = conn.sync();
+
+ var s = cmd.get("sample_post_commit/sentinel");
+ cmd.del("sample_post_commit/sentinel");
+ assertThat(s).isEqualTo("Good Night");
+ }
+ }
+ }
+}
diff --git a/src/test/java/com/github/sttk/sabi_redis/sentinel/SentinelTest.java b/src/test/java/com/github/sttk/sabi_redis/sentinel/SentinelTest.java
new file mode 100644
index 0000000..5b2f323
--- /dev/null
+++ b/src/test/java/com/github/sttk/sabi_redis/sentinel/SentinelTest.java
@@ -0,0 +1,754 @@
+package com.github.sttk.sabi_redis.sentinel;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.fail;
+
+import com.github.sttk.errs.Err;
+import com.github.sttk.sabi.DataHub;
+import io.lettuce.core.RedisURI;
+import io.lettuce.core.resource.DefaultClientResources;
+import java.net.URI;
+import java.net.URISyntaxException;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable;
+
+@DisabledIfEnvironmentVariable(named = "CI", matches = ".*")
+public class SentinelTest {
+ private SentinelTest() {}
+
+ @Test
+ void test_NewRedisSentinelDataSrcWithUriStrings() {
+ try (var data = new DataHub()) {
+ data.uses(
+ "redis",
+ new RedisSentinelDataSrc(
+ "mymaster",
+ "redis://127.0.0.1:26479",
+ "redis://127.0.0.1:26480",
+ "redis://127.0.0.1:26481"));
+ data.run(d -> {});
+ } catch (Err e) {
+ fail(e);
+ }
+ }
+
+ @Test
+ void test_NewRedisSentinelDataSrcWithUriStringsButInvalidAddr_indexIs0() {
+ try (var data = new DataHub()) {
+ data.uses(
+ "redis",
+ new RedisSentinelDataSrc(
+ "mymaster", "xxxx", "redis://127.0.0.1:26480", "redis://127.0.0.1:26481"));
+ data.run(d -> {});
+ fail();
+ } catch (Err err) {
+ switch (err.getReason()) {
+ case DataHub.FailToSetupLocalDataSrcs reason -> {
+ assertThat(reason.errors()).hasSize(1);
+ var err2 = reason.errors().get("redis");
+ switch (err2.getReason()) {
+ case RedisSentinelDataSrc.FailToCreateRedisURI reason2 -> {
+ assertThat(reason2.uri()).isEqualTo("xxxx");
+ assertThat(reason2.index()).isEqualTo(0);
+ assertThat(err2.getCause().toString())
+ .isEqualTo("java.lang.IllegalArgumentException: URI scheme must not be null");
+ }
+ default -> fail(err);
+ }
+ }
+ default -> fail(err);
+ }
+ }
+ }
+
+ @Test
+ void test_NewRedisSentinelDataSrcWithUriStringsButInvalidAddr_indexIs1() {
+ try (var data = new DataHub()) {
+ data.uses(
+ "redis",
+ new RedisSentinelDataSrc(
+ "mymaster", "redis://127.0.0.1:26479", "xxxx", "redis://127.0.0.1:26481"));
+ data.run(d -> {});
+ fail();
+ } catch (Err err) {
+ switch (err.getReason()) {
+ case DataHub.FailToSetupLocalDataSrcs reason -> {
+ assertThat(reason.errors()).hasSize(1);
+ var err2 = reason.errors().get("redis");
+ switch (err2.getReason()) {
+ case RedisSentinelDataSrc.FailToCreateRedisURI reason2 -> {
+ assertThat(reason2.uri()).isEqualTo("xxxx");
+ assertThat(reason2.index()).isEqualTo(1);
+ assertThat(err2.getCause().toString())
+ .isEqualTo("java.lang.IllegalArgumentException: URI scheme must not be null");
+ }
+ default -> fail(err);
+ }
+ }
+ default -> fail(err);
+ }
+ }
+ }
+
+ @Test
+ void test_NewRedisSentinelDataSrcWithUriStringsButAddrIsZero() {
+ try (var data = new DataHub()) {
+ data.uses("redis", new RedisSentinelDataSrc("mymaster", new String[0]));
+ data.run(d -> {});
+ fail();
+ } catch (Err err) {
+ switch (err.getReason()) {
+ case DataHub.FailToSetupLocalDataSrcs reason -> {
+ assertThat(reason.errors()).hasSize(1);
+ var err2 = reason.errors().get("redis");
+ switch (err2.getReason()) {
+ case RedisSentinelDataSrc.FailToBuildSentinelRedisURI reason2 -> {
+ assertThat(reason2.builder()).isNotNull();
+ assertThat(err2.getCause().toString())
+ .isEqualTo(
+ "java.lang.IllegalStateException: Cannot build a RedisURI. One of the following must be provided Host, Socket or Sentinel");
+ }
+ default -> fail(err);
+ }
+ }
+ default -> fail(err);
+ }
+ }
+ }
+
+ @Test
+ void test_NewRedisSentinelDataSrcWithUriStringsButNotFoundAddr() {
+ try (var data = new DataHub()) {
+ data.uses(
+ "redis",
+ new RedisSentinelDataSrc(
+ "mymaster",
+ "redis://127.0.0.1:9999",
+ "redis://127.0.0.1:9998",
+ "redis://127.0.0.1:9997"));
+ data.run(d -> {});
+ fail();
+ } catch (Err err) {
+ switch (err.getReason()) {
+ case DataHub.FailToSetupLocalDataSrcs reason -> {
+ assertThat(reason.errors()).hasSize(1);
+ var err2 = reason.errors().get("redis");
+ switch (err2.getReason()) {
+ case RedisSentinelDataSrc.FailToConnectToRedis reason2 -> {
+ assertThat(reason2.redisURI().getHost()).isEqualTo("127.0.0.1");
+ assertThat(reason2.redisURI().getPort()).isEqualTo(9999);
+ assertThat(reason2.clientResources()).isNull();
+ assertThat(err2.getCause().toString())
+ .isEqualTo(
+ "io.lettuce.core.RedisConnectionException: Cannot connect to a Redis Sentinel: [redis://127.0.0.1:9998, redis://127.0.0.1:9997]");
+ }
+ default -> fail(err);
+ }
+ }
+ default -> fail(err);
+ }
+ }
+ }
+
+ @Test
+ void test_NewRedisSentinelDataSrcWithUriStringsButMasterIdIsNull() {
+ try (var data = new DataHub()) {
+ data.uses(
+ "redis",
+ new RedisSentinelDataSrc(
+ (String) null,
+ "redis://127.0.0.1:26479",
+ "redis://127.0.0.1:26480",
+ "redis://127.0.0.1:26481"));
+ data.run(d -> {});
+ fail();
+ } catch (Err err) {
+ switch (err.getReason()) {
+ case DataHub.FailToSetupLocalDataSrcs reason -> {
+ assertThat(reason.errors()).hasSize(1);
+ var err2 = reason.errors().get("redis");
+ switch (err2.getReason()) {
+ case RedisSentinelDataSrc.BadMasterId reason2 -> {
+ assertThat(reason2.masterId()).isNull();
+ assertThat(err2.getCause().toString())
+ .isEqualTo(
+ "java.lang.IllegalArgumentException: Sentinel master id must not empty");
+ }
+ default -> fail(err);
+ }
+ }
+ default -> fail(err);
+ }
+ }
+ }
+
+ @Test
+ void test_NewRedisSentinelDataSrcWithURIs() {
+ try (var data = new DataHub()) {
+ data.uses(
+ "redis",
+ new RedisSentinelDataSrc(
+ "mymaster",
+ new URI("redis://127.0.0.1:26479"),
+ new URI("redis://127.0.0.1:26480"),
+ new URI("redis://127.0.0.1:26481")));
+ data.run(d -> {});
+ } catch (Err e) {
+ fail(e);
+ } catch (URISyntaxException e) {
+ fail(e);
+ }
+ }
+
+ @Test
+ void test_NewRedisSentinelDataSrcWithURIsButInvalidAddr() {
+ try (var data = new DataHub()) {
+ data.uses(
+ "redis",
+ new RedisSentinelDataSrc(
+ "mymaster",
+ new URI("redis://127.0.0.1:26479"),
+ new URI("xxxx"),
+ new URI("redis://127.0.0.1:26481")));
+ data.run(d -> {});
+ fail();
+ } catch (Err err) {
+ switch (err.getReason()) {
+ case DataHub.FailToSetupLocalDataSrcs reason -> {
+ assertThat(reason.errors()).hasSize(1);
+ var err2 = reason.errors().get("redis");
+ switch (err2.getReason()) {
+ case RedisSentinelDataSrc.FailToCreateRedisURI reason2 -> {
+ assertThat(reason2.uri()).isEqualTo("xxxx");
+ assertThat(reason2.index()).isEqualTo(1);
+ assertThat(err2.getCause().toString())
+ .isEqualTo("java.lang.IllegalArgumentException: URI scheme must not be null");
+ }
+ default -> fail(err);
+ }
+ }
+ default -> fail(err);
+ }
+ } catch (URISyntaxException e) {
+ fail(e);
+ }
+ }
+
+ @Test
+ void test_NewRedisSentinelDataSrcWithURIsButNotFoundAddr() {
+ try (var data = new DataHub()) {
+ data.uses(
+ "redis",
+ new RedisSentinelDataSrc(
+ "mymaster",
+ new URI("redis://127.0.0.1:9999"),
+ new URI("redis://127.0.0.1:9998"),
+ new URI("redis://127.0.0.1:9997")));
+ data.run(d -> {});
+ fail();
+ } catch (Err err) {
+ switch (err.getReason()) {
+ case DataHub.FailToSetupLocalDataSrcs reason -> {
+ assertThat(reason.errors()).hasSize(1);
+ var err2 = reason.errors().get("redis");
+ switch (err2.getReason()) {
+ case RedisSentinelDataSrc.FailToConnectToRedis reason2 -> {
+ assertThat(reason2.redisURI().getHost()).isEqualTo("127.0.0.1");
+ assertThat(reason2.redisURI().getPort()).isEqualTo(9999);
+ assertThat(reason2.clientResources()).isNull();
+ assertThat(err2.getCause().toString())
+ .isEqualTo(
+ "io.lettuce.core.RedisConnectionException: Cannot connect to a Redis Sentinel: [redis://127.0.0.1:9998, redis://127.0.0.1:9997]");
+ }
+ default -> fail(err);
+ }
+ }
+ default -> fail(err);
+ }
+ } catch (URISyntaxException e) {
+ fail(e);
+ }
+ }
+
+ @Test
+ void test_NewRedisSentinelDataSrcWithRedisURIs() {
+ try (var data = new DataHub()) {
+ data.uses(
+ "redis",
+ new RedisSentinelDataSrc(
+ "mymaster",
+ RedisURI.create("redis://127.0.0.1:26479"),
+ RedisURI.create("redis://127.0.0.1:26480"),
+ RedisURI.create("redis://127.0.0.1:26481")));
+ data.run(d -> {});
+ } catch (Err e) {
+ fail(e);
+ }
+ }
+
+ @Test
+ void test_NewRedisSentinelDataSrcWithRedisURIsButInvalidAddr_indexIs0() {
+ try (var data = new DataHub()) {
+ data.uses(
+ "redis",
+ new RedisSentinelDataSrc(
+ "mymaster",
+ (RedisURI) null,
+ RedisURI.create("redis://127.0.0.1:26480"),
+ RedisURI.create("redis://127.0.0.1:26481")));
+ data.run(d -> {});
+ fail();
+ } catch (Err err) {
+ switch (err.getReason()) {
+ case DataHub.FailToSetupLocalDataSrcs reason -> {
+ assertThat(reason.errors()).hasSize(1);
+ var err2 = reason.errors().get("redis");
+ switch (err2.getReason()) {
+ case RedisSentinelDataSrc.BadRedisURI reason2 -> {
+ assertThat(reason2.redisURI()).isNull();
+ assertThat(err2.getCause().toString())
+ .isEqualTo(
+ "java.lang.IllegalArgumentException: Source RedisURI must not be null");
+ }
+ default -> fail(err);
+ }
+ }
+ default -> fail(err);
+ }
+ }
+ }
+
+ @Test
+ void test_NewRedisSentinelDataSrcWithRedisURIsButInvalidAddr_indexIs1() {
+ try (var data = new DataHub()) {
+ data.uses(
+ "redis",
+ new RedisSentinelDataSrc(
+ "mymaster",
+ RedisURI.create("redis://127.0.0.1:26479"),
+ (RedisURI) null,
+ RedisURI.create("redis://127.0.0.1:26481")));
+ data.run(d -> {});
+ fail();
+ } catch (Err err) {
+ switch (err.getReason()) {
+ case DataHub.FailToSetupLocalDataSrcs reason -> {
+ assertThat(reason.errors()).hasSize(1);
+ var err2 = reason.errors().get("redis");
+ switch (err2.getReason()) {
+ case RedisSentinelDataSrc.BadRedisURI reason2 -> {
+ assertThat(reason2.redisURI()).isNull();
+ assertThat(err2.getCause().toString())
+ .isEqualTo("java.lang.IllegalArgumentException: Redis URI must not be null");
+ }
+ default -> fail(err);
+ }
+ }
+ default -> fail(err);
+ }
+ }
+ }
+
+ @Test
+ void test_NewRedisSentinelDataSrcWithRedisURIsButAddrIsZero() {
+ try (var data = new DataHub()) {
+ data.uses("redis", new RedisSentinelDataSrc("mymaster", new RedisURI[0]));
+ data.run(d -> {});
+ fail();
+ } catch (Err err) {
+ switch (err.getReason()) {
+ case DataHub.FailToSetupLocalDataSrcs reason -> {
+ assertThat(reason.errors()).hasSize(1);
+ var err2 = reason.errors().get("redis");
+ switch (err2.getReason()) {
+ case RedisSentinelDataSrc.FailToBuildSentinelRedisURI reason2 -> {
+ assertThat(reason2.builder()).isNotNull();
+ assertThat(err2.getCause().toString())
+ .isEqualTo(
+ "java.lang.IllegalStateException: Cannot build a RedisURI. One of the following must be provided Host, Socket or Sentinel");
+ }
+ default -> fail(err);
+ }
+ }
+ default -> fail(err);
+ }
+ }
+ }
+
+ @Test
+ void test_NewRedisSentinelDataSrcWithRedisURIsButNotFoundAddr() {
+ try (var data = new DataHub()) {
+ data.uses(
+ "redis",
+ new RedisSentinelDataSrc(
+ "mymaster",
+ RedisURI.create("redis://127.0.0.1:9999"),
+ RedisURI.create("redis://127.0.0.1:9998"),
+ RedisURI.create("redis://127.0.0.1:9997")));
+ data.run(d -> {});
+ fail();
+ } catch (Err err) {
+ switch (err.getReason()) {
+ case DataHub.FailToSetupLocalDataSrcs reason -> {
+ assertThat(reason.errors()).hasSize(1);
+ var err2 = reason.errors().get("redis");
+ switch (err2.getReason()) {
+ case RedisSentinelDataSrc.FailToConnectToRedis reason2 -> {
+ assertThat(reason2.redisURI().getHost()).isEqualTo("127.0.0.1");
+ assertThat(reason2.redisURI().getPort()).isEqualTo(9999);
+ assertThat(reason2.clientResources()).isNull();
+ assertThat(err2.getCause().toString())
+ .isEqualTo(
+ "io.lettuce.core.RedisConnectionException: Cannot connect to a Redis Sentinel: [redis://127.0.0.1:9998, redis://127.0.0.1:9997]");
+ }
+ default -> fail(err);
+ }
+ }
+ default -> fail(err);
+ }
+ }
+ }
+
+ @Test
+ void test_NewRedisSentinelDataSrcWithRedisURIsButMasterIdIsNull() {
+ try (var data = new DataHub()) {
+ data.uses(
+ "redis",
+ new RedisSentinelDataSrc(
+ (String) null,
+ RedisURI.create("redis://127.0.0.1:26479"),
+ RedisURI.create("redis://127.0.0.1:26480"),
+ RedisURI.create("redis://127.0.0.1:26481")));
+ data.run(d -> {});
+ fail();
+ } catch (Err err) {
+ switch (err.getReason()) {
+ case DataHub.FailToSetupLocalDataSrcs reason -> {
+ assertThat(reason.errors()).hasSize(1);
+ var err2 = reason.errors().get("redis");
+ switch (err2.getReason()) {
+ case RedisSentinelDataSrc.BadMasterId reason2 -> {
+ assertThat(reason2.masterId()).isNull();
+ assertThat(err2.getCause().toString())
+ .isEqualTo(
+ "java.lang.IllegalArgumentException: Sentinel master id must not empty");
+ }
+ default -> fail(err);
+ }
+ }
+ default -> fail(err);
+ }
+ }
+ }
+
+ @Test
+ void test_NewRedisSentinelDataSrcWithClientResourcesAndUriStrings() {
+ var cr = DefaultClientResources.create();
+ try (var data = new DataHub()) {
+ data.uses(
+ "redis",
+ new RedisSentinelDataSrc(
+ cr,
+ "mymaster",
+ "redis://127.0.0.1:26479",
+ "redis://127.0.0.1:26480",
+ "redis://127.0.0.1:26481"));
+ data.run(d -> {});
+ } catch (Err e) {
+ fail(e);
+ } finally {
+ cr.shutdown();
+ }
+ }
+
+ @Test
+ void test_NewRedisSentinelDataSrcWithClientResourcesAndUriStringsButInvalidAddr() {
+ var cr = DefaultClientResources.create();
+ try (var data = new DataHub()) {
+ data.uses(
+ "redis",
+ new RedisSentinelDataSrc(
+ cr, "mymaster", "redis://127.0.0.1:26479", "xxxx", "redis://127.0.0.1:26481"));
+ data.run(d -> {});
+ fail();
+ } catch (Err err) {
+ switch (err.getReason()) {
+ case DataHub.FailToSetupLocalDataSrcs reason -> {
+ assertThat(reason.errors()).hasSize(1);
+ var err2 = reason.errors().get("redis");
+ switch (err2.getReason()) {
+ case RedisSentinelDataSrc.FailToCreateRedisURI reason2 -> {
+ assertThat(reason2.uri()).isEqualTo("xxxx");
+ assertThat(reason2.index()).isEqualTo(1);
+ assertThat(err2.getCause().toString())
+ .isEqualTo("java.lang.IllegalArgumentException: URI scheme must not be null");
+ }
+ default -> fail(err);
+ }
+ }
+ default -> fail(err);
+ }
+ } finally {
+ cr.shutdown();
+ }
+ }
+
+ @Test
+ void test_NewRedisSentinelDataSrcWithClientResourcesAndUriStringsButNotFoundAddr() {
+ var cr = DefaultClientResources.create();
+ try (var data = new DataHub()) {
+ data.uses(
+ "redis",
+ new RedisSentinelDataSrc(
+ cr,
+ "mymaster",
+ "redis://127.0.0.1:9999",
+ "redis://127.0.0.1:9998",
+ "redis://127.0.0.1:9997"));
+ data.run(d -> {});
+ fail();
+ } catch (Err err) {
+ switch (err.getReason()) {
+ case DataHub.FailToSetupLocalDataSrcs reason -> {
+ assertThat(reason.errors()).hasSize(1);
+ var err2 = reason.errors().get("redis");
+ switch (err2.getReason()) {
+ case RedisSentinelDataSrc.FailToConnectToRedis reason2 -> {
+ assertThat(reason2.redisURI().getHost()).isEqualTo("127.0.0.1");
+ assertThat(reason2.redisURI().getPort()).isEqualTo(9999);
+ assertThat(reason2.clientResources()).isNotNull();
+ assertThat(err2.getCause().toString())
+ .isEqualTo(
+ "io.lettuce.core.RedisConnectionException: Cannot connect to a Redis Sentinel: [redis://127.0.0.1:9998, redis://127.0.0.1:9997]");
+ }
+ default -> fail(err);
+ }
+ }
+ default -> fail(err);
+ }
+ } finally {
+ cr.shutdown();
+ }
+ }
+
+ @Test
+ void test_NewRedisSentinelDataSrcWithClientResourcesAndURIs() {
+ var cr = DefaultClientResources.create();
+ try (var data = new DataHub()) {
+ data.uses(
+ "redis",
+ new RedisSentinelDataSrc(
+ cr,
+ "mymaster",
+ new URI("redis://127.0.0.1:26479"),
+ new URI("redis://127.0.0.1:26480"),
+ new URI("redis://127.0.0.1:26481")));
+ data.run(d -> {});
+ } catch (Err e) {
+ fail(e);
+ } catch (URISyntaxException e) {
+ fail(e);
+ } finally {
+ cr.shutdown();
+ }
+ }
+
+ @Test
+ void test_NewRedisSentinelDataSrcWithClientResourcesAndURIsButInvalidAddr() {
+ var cr = DefaultClientResources.create();
+ try (var data = new DataHub()) {
+ data.uses(
+ "redis",
+ new RedisSentinelDataSrc(
+ cr,
+ "mymaster",
+ new URI("redis://127.0.0.1:26479"),
+ new URI("xxxx"),
+ new URI("redis://127.0.0.1:26481")));
+ data.run(d -> {});
+ fail();
+ } catch (Err err) {
+ switch (err.getReason()) {
+ case DataHub.FailToSetupLocalDataSrcs reason -> {
+ assertThat(reason.errors()).hasSize(1);
+ var err2 = reason.errors().get("redis");
+ switch (err2.getReason()) {
+ case RedisSentinelDataSrc.FailToCreateRedisURI reason2 -> {
+ assertThat(reason2.uri()).isEqualTo("xxxx");
+ assertThat(reason2.index()).isEqualTo(1);
+ assertThat(err2.getCause().toString())
+ .isEqualTo("java.lang.IllegalArgumentException: URI scheme must not be null");
+ }
+ default -> fail(err);
+ }
+ }
+ default -> fail(err);
+ }
+ } catch (URISyntaxException e) {
+ fail(e);
+ } finally {
+ cr.shutdown();
+ }
+ }
+
+ @Test
+ void test_NewRedisSentinelDataSrcWithClientResourcesAndURIsButNotFoundAddr() {
+ var cr = DefaultClientResources.create();
+ try (var data = new DataHub()) {
+ data.uses(
+ "redis",
+ new RedisSentinelDataSrc(
+ cr,
+ "mymaster",
+ new URI("redis://127.0.0.1:9999"),
+ new URI("redis://127.0.0.1:9998"),
+ new URI("redis://127.0.0.1:9997")));
+ data.run(d -> {});
+ fail();
+ } catch (Err err) {
+ switch (err.getReason()) {
+ case DataHub.FailToSetupLocalDataSrcs reason -> {
+ assertThat(reason.errors()).hasSize(1);
+ var err2 = reason.errors().get("redis");
+ switch (err2.getReason()) {
+ case RedisSentinelDataSrc.FailToConnectToRedis reason2 -> {
+ assertThat(reason2.redisURI().getHost()).isEqualTo("127.0.0.1");
+ assertThat(reason2.redisURI().getPort()).isEqualTo(9999);
+ assertThat(reason2.clientResources()).isNotNull();
+ assertThat(err2.getCause().toString())
+ .isEqualTo(
+ "io.lettuce.core.RedisConnectionException: Cannot connect to a Redis Sentinel: [redis://127.0.0.1:9998, redis://127.0.0.1:9997]");
+ }
+ default -> fail(err);
+ }
+ }
+ default -> fail(err);
+ }
+ } catch (URISyntaxException e) {
+ fail(e);
+ } finally {
+ cr.shutdown();
+ }
+ }
+
+ @Test
+ void test_NewRedisSentinelDataSrcWithClientResourcesAndRedisURIs() {
+ var cr = DefaultClientResources.create();
+ try (var data = new DataHub()) {
+ data.uses(
+ "redis",
+ new RedisSentinelDataSrc(
+ cr,
+ "mymaster",
+ RedisURI.create("redis://127.0.0.1:26479"),
+ RedisURI.create("redis://127.0.0.1:26480"),
+ RedisURI.create("redis://127.0.0.1:26481")));
+ data.run(d -> {});
+ } catch (Err e) {
+ fail(e);
+ } finally {
+ cr.shutdown();
+ }
+ }
+
+ @Test
+ void test_NewRedisSentinelDataSrcWithClientResourcesAndRedisURIsButNotFoundAddr() {
+ var cr = DefaultClientResources.create();
+ try (var data = new DataHub()) {
+ data.uses(
+ "redis",
+ new RedisSentinelDataSrc(
+ cr,
+ "mymaster",
+ RedisURI.create("redis://127.0.0.1:9999"),
+ RedisURI.create("redis://127.0.0.1:9998"),
+ RedisURI.create("redis://127.0.0.1:9997")));
+ data.run(d -> {});
+ fail();
+ } catch (Err err) {
+ switch (err.getReason()) {
+ case DataHub.FailToSetupLocalDataSrcs reason -> {
+ assertThat(reason.errors()).hasSize(1);
+ var err2 = reason.errors().get("redis");
+ switch (err2.getReason()) {
+ case RedisSentinelDataSrc.FailToConnectToRedis reason2 -> {
+ assertThat(reason2.redisURI().getHost()).isEqualTo("127.0.0.1");
+ assertThat(reason2.redisURI().getPort()).isEqualTo(9999);
+ assertThat(reason2.clientResources()).isNotNull();
+ assertThat(err2.getCause().toString())
+ .isEqualTo(
+ "io.lettuce.core.RedisConnectionException: Cannot connect to a Redis Sentinel: [redis://127.0.0.1:9998, redis://127.0.0.1:9997]");
+ }
+ default -> fail(err);
+ }
+ }
+ default -> fail(err);
+ }
+ } finally {
+ cr.shutdown();
+ }
+ }
+
+ @Nested
+ class ForCoverage {
+ @Test
+ void testRedisSentinelDataConn() {
+ var conn = new RedisSentinelDataConn(null);
+ conn.rollback(null);
+
+ conn.addPreCommit(
+ _conn -> {
+ throw new Err("bad");
+ });
+ try {
+ conn.preCommit(null);
+ fail();
+ } catch (Err err) {
+ assertThat(err.getReason()).isEqualTo("bad");
+ }
+
+ conn.addPostCommit(
+ _conn -> {
+ throw new Err("bad");
+ });
+ conn.postCommit(null);
+
+ conn.addForceBack(
+ _conn -> {
+ throw new Err("bad");
+ });
+ conn.forceBack(null);
+ }
+
+ @Test
+ void testRedisDataSrc() {
+ var ds =
+ new RedisSentinelDataSrc(
+ "mymaster",
+ "redis://127.0.0.1:26479",
+ "redis://127.0.0.1:26480",
+ "redis://127.0.0.1:26481");
+ try {
+ ds.createDataConn();
+ fail();
+ } catch (Err err) {
+ assertThat(err.getReason()).isEqualTo(new RedisSentinelDataSrc.NotSetupYet());
+ }
+ ds.close();
+
+ try {
+ ds.setup(null);
+ } catch (Err err) {
+ fail(err);
+ }
+ try {
+ ds.setup(null);
+ fail();
+ } catch (Err err) {
+ assertThat(err.getReason()).isEqualTo(new RedisSentinelDataSrc.AlreadySetup());
+ }
+ ds.close();
+ }
+ }
+}