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(); + } + } +}