This is an automated email from the ASF dual-hosted git repository.

hqtran pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit 2d12012447f610532d5e557dc862700cab4727b8
Author: Quan Tran <hqt...@linagora.com>
AuthorDate: Mon Mar 31 14:49:45 2025 +0700

    JAMES-4124 Introduce KvrocksSentinelExtension
---
 .../backends/redis/KvrocksSentinelExtension.java   | 240 +++++++++++++++++++++
 .../src/test/resources/kvrocks/master/kvrocks.conf |   4 +
 .../test/resources/kvrocks/replica/kvrocks.conf    |   5 +
 .../test/resources/kvrocks/sentinel/sentinel.conf  |   8 +
 4 files changed, 257 insertions(+)

diff --git 
a/backends-common/redis/src/test/java/org/apache/james/backends/redis/KvrocksSentinelExtension.java
 
b/backends-common/redis/src/test/java/org/apache/james/backends/redis/KvrocksSentinelExtension.java
new file mode 100644
index 0000000000..b54f25b263
--- /dev/null
+++ 
b/backends-common/redis/src/test/java/org/apache/james/backends/redis/KvrocksSentinelExtension.java
@@ -0,0 +1,240 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ * http://www.apache.org/licenses/LICENSE-2.0                   *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.backends.redis;
+
+import static org.apache.james.backends.redis.DockerRedis.DEFAULT_IMAGE_NAME;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+import jakarta.inject.Singleton;
+
+import org.apache.james.GuiceModuleTestExtension;
+import org.apache.james.util.Runnables;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.api.extension.ParameterContext;
+import org.junit.jupiter.api.extension.ParameterResolutionException;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.Network;
+import 
org.testcontainers.containers.wait.strategy.DockerHealthcheckWaitStrategy;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.MountableFile;
+
+import com.github.dockerjava.api.model.HealthCheck;
+import com.github.fge.lambdas.Throwing;
+import com.google.inject.AbstractModule;
+import com.google.inject.Module;
+import com.google.inject.Provides;
+
+import io.lettuce.core.ReadFrom;
+
+public class KvrocksSentinelExtension implements GuiceModuleTestExtension {
+    public static final int SENTINEL_PORT = 26379;
+    public static final String SENTINEL_PASSWORD = "321";
+
+    public static class KvrocksMasterReplicaContainerList extends 
ArrayList<GenericContainer> {
+        public KvrocksMasterReplicaContainerList(Collection<? extends 
GenericContainer> c) {
+            super(c);
+        }
+
+        public void pauseMasterNode() {
+            GenericContainer container = this.get(0);
+            
container.getDockerClient().pauseContainerCmd(container.getContainerId()).exec();
+        }
+
+        public void unPauseMasterNode() {
+            GenericContainer container = this.get(0);
+            if 
(container.getDockerClient().inspectContainerCmd(container.getContainerId())
+                .exec()
+                .getState()
+                .getPaused()) {
+                
container.getDockerClient().unpauseContainerCmd(container.getContainerId()).exec();
+            }
+        }
+    }
+
+    public static class RedisSentinelContainerList extends 
ArrayList<GenericContainer> {
+        private final SentinelRedisConfiguration sentinelRedisConfiguration;
+
+        public RedisSentinelContainerList(Collection<? extends 
GenericContainer> c) {
+            super(c);
+            String sentinelURI = createRedisSentinelURI();
+            sentinelRedisConfiguration = 
SentinelRedisConfiguration.from(sentinelURI,
+                ReadFrom.MASTER,
+                SENTINEL_PASSWORD);
+        }
+
+        public SentinelRedisConfiguration getRedisConfiguration() {
+            return sentinelRedisConfiguration;
+        }
+
+        public void pauseFirstNode() {
+            GenericContainer container = this.get(0);
+            
container.getDockerClient().pauseContainerCmd(container.getContainerId()).exec();
+        }
+
+        public void unPauseFirstNode() {
+            GenericContainer container = this.get(0);
+            if 
(container.getDockerClient().inspectContainerCmd(container.getContainerId())
+                .exec()
+                .getState()
+                .getPaused()) {
+                
container.getDockerClient().unpauseContainerCmd(container.getContainerId()).exec();
+            }
+        }
+
+        private String createRedisSentinelURI() {
+            StringBuilder sb = new StringBuilder();
+            sb.append("redis-sentinel://123@");
+            sb.append(this.stream().map(container -> container.getHost() + ":" 
+ container.getMappedPort(SENTINEL_PORT))
+                .collect(Collectors.joining(",")));
+            sb.append("?sentinelMasterId=mymaster");
+            return sb.toString();
+        }
+    }
+
+    public record KvrocksSentinel(KvrocksMasterReplicaContainerList 
kvrocksMasterReplicaContainerList,
+                                  RedisSentinelContainerList 
redisSentinelContainerList) {
+    }
+
+    final GenericContainer kvrocks1;
+    final GenericContainer kvrocks2;
+    final GenericContainer kvrocks3;
+    final GenericContainer sentinel1;
+    final GenericContainer sentinel2;
+    final GenericContainer sentinel3;
+
+    private KvrocksMasterReplicaContainerList 
kvrocksMasterReplicaContainerList;
+    private RedisSentinelContainerList redisSentinelContainerList;
+    private KvrocksSentinel kvrocksSentinel;
+    private final Network network;
+
+    public KvrocksSentinelExtension() {
+        this.network = Network.newNetwork();
+        kvrocks1 = createKvrocksContainer("kvrocks1", false);
+        kvrocks2 = createKvrocksContainer("kvrocks2", true);
+        kvrocks3 = createKvrocksContainer("kvrocks3", true);
+        sentinel1 = createSentinelContainer("sentinel1");
+        sentinel2 = createSentinelContainer("sentinel2");
+        sentinel3 = createSentinelContainer("sentinel3");
+        kvrocks1.withNetwork(network);
+        kvrocks2.withNetwork(network);
+        kvrocks3.withNetwork(network);
+        sentinel1.withNetwork(network);
+        sentinel2.withNetwork(network);
+        sentinel3.withNetwork(network);
+    }
+
+    @Override
+    public void beforeAll(ExtensionContext extensionContext) {
+        kvrocks1.start();
+        kvrocks2.start();
+        kvrocks3.start();
+        sentinel1.start();
+        sentinel2.start();
+        sentinel3.start();
+        kvrocksMasterReplicaContainerList = new 
KvrocksMasterReplicaContainerList(List.of(kvrocks1, kvrocks2, kvrocks3));
+        redisSentinelContainerList = new 
RedisSentinelContainerList(List.of(sentinel1, sentinel2, sentinel3));
+        kvrocksSentinel = new 
KvrocksSentinel(kvrocksMasterReplicaContainerList, redisSentinelContainerList);
+    }
+
+    @Override
+    public void afterAll(ExtensionContext extensionContext) {
+        Runnables.runParallel(
+            sentinel1::stop,
+            sentinel2::stop,
+            sentinel3::stop,
+            kvrocks1::stop,
+            kvrocks2::stop,
+            kvrocks3::stop);
+        network.close();
+    }
+
+    @Override
+    public void beforeEach(ExtensionContext extensionContext) {
+        kvrocksMasterReplicaContainerList.forEach(Throwing.consumer(container 
-> container.execInContainer("redis-cli", "flushall")));
+    }
+
+    public KvrocksSentinel getRedisSentinelCluster() {
+        return kvrocksSentinel;
+    }
+
+    @Override
+    public Module getModule() {
+        return new AbstractModule() {
+            @Provides
+            @Singleton
+            public RedisConfiguration provideRedisConfiguration() {
+                return redisSentinelContainerList.getRedisConfiguration();
+            }
+        };
+    }
+
+    @Override
+    public boolean supportsParameter(ParameterContext parameterContext, 
ExtensionContext extensionContext) throws ParameterResolutionException {
+        return parameterContext.getParameter().getType() == 
KvrocksSentinel.class;
+    }
+
+    @Override
+    public Object resolveParameter(ParameterContext parameterContext, 
ExtensionContext extensionContext) throws ParameterResolutionException {
+        return kvrocksSentinel;
+    }
+
+    private GenericContainer createKvrocksContainer(String alias, boolean 
isReplica) {
+        GenericContainer genericContainer = new 
GenericContainer<>(DockerKvrocks.DEFAULT_IMAGE_NAME)
+            .withCreateContainerCmdModifier(createContainerCmd -> 
createContainerCmd.withName("james-" + alias + "-test-" + UUID.randomUUID()))
+            .withCreateContainerCmdModifier(cmd -> cmd.withHealthcheck(new 
HealthCheck()
+                .withTest(List.of("CMD-SHELL", "redis-cli -p 6379 PING | grep 
-E '(PONG|NOAUTH)' || exit 1"))
+                .withInterval(Duration.ofSeconds(2).toNanos())
+                .withTimeout(Duration.ofSeconds(3).toNanos())
+                .withStartPeriod(Duration.ofSeconds(30).toNanos())
+                .withRetries(10)))
+            .withNetworkAliases(alias)
+            .waitingFor(new DockerHealthcheckWaitStrategy()
+                .withStartupTimeout(Duration.ofMinutes(2)));
+
+        if (isReplica) {
+            
genericContainer.withCopyFileToContainer(MountableFile.forClasspathResource("kvrocks/replica/kvrocks.conf"),
+                "/var/lib/kvrocks/kvrocks.conf");
+        } else {
+            
genericContainer.withCopyFileToContainer(MountableFile.forClasspathResource("kvrocks/master/kvrocks.conf"),
+                "/var/lib/kvrocks/kvrocks.conf");
+        }
+
+        return genericContainer;
+    }
+
+    private GenericContainer createSentinelContainer(String alias) {
+        GenericContainer genericContainer = new 
GenericContainer<>(DEFAULT_IMAGE_NAME)
+            .withExposedPorts(SENTINEL_PORT)
+            .withCreateContainerCmdModifier(createContainerCmd -> 
createContainerCmd.withName("james-" + alias + "-test-" + UUID.randomUUID()))
+            .withCommand("redis-sentinel /etc/redis/sentinel.conf")
+            .withNetworkAliases(alias)
+            .waitingFor(Wait.forLogMessage(".*monitor master.*", 1)
+                .withStartupTimeout(Duration.ofMinutes(2)));
+        
genericContainer.withCopyFileToContainer(MountableFile.forClasspathResource("kvrocks/sentinel/sentinel.conf"),
+            "/etc/redis/sentinel.conf");
+        return genericContainer;
+    }
+}
\ No newline at end of file
diff --git 
a/backends-common/redis/src/test/resources/kvrocks/master/kvrocks.conf 
b/backends-common/redis/src/test/resources/kvrocks/master/kvrocks.conf
new file mode 100644
index 0000000000..ecec9216ee
--- /dev/null
+++ b/backends-common/redis/src/test/resources/kvrocks/master/kvrocks.conf
@@ -0,0 +1,4 @@
+log-level info
+requirepass 123
+masterauth 123
+port 6379
\ No newline at end of file
diff --git 
a/backends-common/redis/src/test/resources/kvrocks/replica/kvrocks.conf 
b/backends-common/redis/src/test/resources/kvrocks/replica/kvrocks.conf
new file mode 100644
index 0000000000..885a424bb4
--- /dev/null
+++ b/backends-common/redis/src/test/resources/kvrocks/replica/kvrocks.conf
@@ -0,0 +1,5 @@
+log-level info
+slaveof kvrocks1 6379
+masterauth 123
+requirepass 123
+port 6379
\ No newline at end of file
diff --git 
a/backends-common/redis/src/test/resources/kvrocks/sentinel/sentinel.conf 
b/backends-common/redis/src/test/resources/kvrocks/sentinel/sentinel.conf
new file mode 100644
index 0000000000..1c2e164e65
--- /dev/null
+++ b/backends-common/redis/src/test/resources/kvrocks/sentinel/sentinel.conf
@@ -0,0 +1,8 @@
+dir /tmp
+sentinel resolve-hostnames yes
+sentinel monitor mymaster kvrocks1 6379 2
+sentinel auth-pass mymaster 123
+sentinel down-after-milliseconds mymaster 5000
+sentinel failover-timeout mymaster 10000
+sentinel parallel-syncs mymaster 1
+requirepass "321"
\ No newline at end of file


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscr...@james.apache.org
For additional commands, e-mail: notifications-h...@james.apache.org

Reply via email to