From 72fb7fb76b2081348f337beeda16c856c7fb1ca6 Mon Sep 17 00:00:00 2001 From: shingmoyeung <525032303@qq.com> Date: Mon, 7 Aug 2023 14:08:31 +0800 Subject: [PATCH 1/7] [NEW] Added maven-ci.yml [NEW] Added WatcherConstant.java [NEW] Added LettuceRedisWatcher.java [NEW] Added LettuceSubscriber.java [NEW] Added LettuceSubThread.java [NEW] Added LettuceRedisWatcherTest.java [UPDATE] Updated .gitignore [NEW] Added .releaserc.json [NEW] Added maven-settings.xml [NEW] Added pom.xml [UPDATE] Updated README.md --- .github/workflows/maven-ci.yml | 59 ++++ .gitignore | 5 + .releaserc.json | 17 + README.md | 27 +- maven-settings.xml | 22 ++ pom.xml | 291 ++++++++++++++++++ .../watcher/lettuce/LettuceRedisWatcher.java | 156 ++++++++++ .../watcher/lettuce/LettuceSubThread.java | 124 ++++++++ .../watcher/lettuce/LettuceSubscriber.java | 27 ++ .../lettuce/constants/WatcherConstant.java | 19 ++ .../casbin/test/LettuceRedisWatcherTest.java | 63 ++++ 11 files changed, 809 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/maven-ci.yml create mode 100644 .releaserc.json create mode 100644 maven-settings.xml create mode 100644 pom.xml create mode 100644 src/main/java/org/casbin/watcher/lettuce/LettuceRedisWatcher.java create mode 100644 src/main/java/org/casbin/watcher/lettuce/LettuceSubThread.java create mode 100644 src/main/java/org/casbin/watcher/lettuce/LettuceSubscriber.java create mode 100644 src/main/java/org/casbin/watcher/lettuce/constants/WatcherConstant.java create mode 100644 src/test/java/org/casbin/test/LettuceRedisWatcherTest.java diff --git a/.github/workflows/maven-ci.yml b/.github/workflows/maven-ci.yml new file mode 100644 index 0000000..12d6f9f --- /dev/null +++ b/.github/workflows/maven-ci.yml @@ -0,0 +1,59 @@ +name: build + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + services: + redis: + image: redis + ports: + - 6378:6378 + + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: '0' + + - name: Set up Redis with password + uses: getong/redis-action@v1 + with: + redis version: 'latest' + host port: 6379 + container port: 6379 + redis password: 'foobared' + + - name: Set up JDK 1.8 + uses: actions/setup-java@v1 + with: + java-version: 1.8 + server-username: OSSRH_USERNAME + server-password: OSSRH_PASSWORD + gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} + gpg-passphrase: GPG_PASSPHRASE + + - name: Build with Maven + run: mvn clean test cobertura:cobertura + + - name: Codecov + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + + - name: Set up Node.js + uses: actions/setup-node@v2 + with: + node-version: 16 + + - name: Sematic Release + run: | + npm install -g @conveyal/maven-semantic-release semantic-release + semantic-release --prepare @conveyal/maven-semantic-release --publish @semantic-release/github,@conveyal/maven-semantic-release --verify-conditions @semantic-release/github,@conveyal/maven-semantic-release --verify-release @conveyal/maven-semantic-release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GPG_KEY_NAME: ${{ secrets.GPG_KEY_NAME }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} + OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} diff --git a/.gitignore b/.gitignore index 524f096..d909bae 100644 --- a/.gitignore +++ b/.gitignore @@ -18,7 +18,12 @@ *.zip *.tar.gz *.rar +*.iml # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* replay_pid* + +.idea +target/ +out/ \ No newline at end of file diff --git a/.releaserc.json b/.releaserc.json new file mode 100644 index 0000000..8c262e0 --- /dev/null +++ b/.releaserc.json @@ -0,0 +1,17 @@ +{ + "debug": true, + "dryRun": false, + "branches": [ + "+([0-9])?(.{+([0-9]),x}).x", + "master", + { + "name": "beta", + "prerelease": true + } + ], + "plugins": [ + "@semantic-release/commit-analyzer", + "@semantic-release/release-notes-generator", + "@semantic-release/github" + ] +} \ No newline at end of file diff --git a/README.md b/README.md index 99ccd63..6ada743 100644 --- a/README.md +++ b/README.md @@ -1 +1,26 @@ -# lettuce-redis-watcher \ No newline at end of file +# lettuce-redis-watcher +--- + +[![GitHub Actions](https://github.com/jcasbin/lettuce-redis-watcher/actions/workflows/maven-ci.yml/badge.svg)](https://github.com/jcasbin/lettuce-redis-watcher/actions/workflows/maven-ci.yml) +![License](https://img.shields.io/github/license/jcasbin/lettuce-redis-watcher) +[![Javadoc](https://javadoc.io/badge2/org.casbin/jcasbin-lettuce-redis-watcher/javadoc.svg)](https://javadoc.io/doc/org.casbin/jcasbin-lettuce-redis-watcher) +[![codecov](https://codecov.io/gh/jcasbin/lettuce-redis-watcher/branch/master/graph/badge.svg?token=ENt9xr4nFg)](https://codecov.io/gh/jcasbin/lettuce-redis-watcher) +[![codebeat badge](https://codebeat.co/badges/8b3da1c4-3a61-4123-a3d4-002b2598a297)](https://codebeat.co/projects/github-com-jcasbin-lettuce-redis-watcher-master) +[![Maven Central](https://img.shields.io/maven-central/v/org.casbin/jcasbin-lettuce-redis-watcher.svg)](https://mvnrepository.com/artifact/org.casbin/jcasbin-lettuce-redis-watcher/latest) +[![Release](https://img.shields.io/github/release/jcasbin/lettuce-redis-watcher.svg)](https://github.com/jcasbin/lettuce-redis-watcher/releases/latest) +[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/casbin/lobby) + +--- +[![Security Status](https://www.murphysec.com/platform3/v31/badge/1688427475171692544.svg)](https://www.murphysec.com/console/report/1688411419949424640/1688427475171692544) + + +Lettuce Redis Watcher is a [Redis](http://redis.io) watcher for [jCasbin](https://github.com/casbin/jcasbin). + +## Getting Help + +- [jCasbin](https://github.com/casbin/jCasbin) +- [Lettuce](https://lettuce.io) + +## License + +This project is under Apache 2.0 License. See the [LICENSE](https://github.com/jcasbin/lettuce-redis-watcher/blob/master/LICENSE) file for the full license text. \ No newline at end of file diff --git a/maven-settings.xml b/maven-settings.xml new file mode 100644 index 0000000..410aee6 --- /dev/null +++ b/maven-settings.xml @@ -0,0 +1,22 @@ + + + + ossrh + ${OSSRH_USERNAME} + ${OSSRH_PASSWORD} + + + + + ossrh + + true + + + gpg + ${GPG_KEY_NAME} + ${GPG_PASSPHRASE} + + + + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..387332b --- /dev/null +++ b/pom.xml @@ -0,0 +1,291 @@ + + + 4.0.0 + org.casbin + jcasbin-lettuce-redis-watcher + 1.0.0 + + + org.sonatype.oss + oss-parent + 7 + + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + https://github.com/jcasbin/lettuce-redis-watcher + scm:git@github.com:jcasbin/lettuce-redis-watcher.git + scm:git:https://github.com/jcasbin/lettuce-redis-watcher.git + + + + Shingmo Yeung + 525032303@qq.com + https://github.com/ShingmoYeung + + + + Github + https://github.com/jcasbin/lettuce-redis-watcher/issues + + + + + ossrh + https://oss.sonatype.org/content/repositories/snapshots + + + ossrh + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + + + 1.8 + UTF-8 + ${java.version} + ${java.version} + ${source.encoding} + 3.3.0 + 3.11.0 + 3.1.0 + 3.5.0 + 2.7 + 1.6.13 + UTF-8 + ${source.encoding} + + 1.35.0 + 6.2.5.RELEASE + 2.11.1 + 3.13.0 + 2.0.7 + 5.3.3 + 4.1.96.Final + 4.13.2 + + + + + + org.casbin + jcasbin + ${jcasbin.version} + + + io.lettuce + lettuce-core + ${lettuce-core.version} + + + org.apache.commons + commons-pool2 + ${commons-pool2.version} + + + org.apache.commons + commons-lang3 + ${commons-lang3.version} + + + org.slf4j + slf4j-api + ${slf4j.version} + + + com.googlecode.aviator + aviator + ${aviator.version} + + + io.netty + netty-common + ${netty.version} + + + io.netty + netty-handler + ${netty.version} + + + io.netty + netty-transport + ${netty.version} + + + io.netty + netty-resolver-dns + ${netty.version} + true + + + io.netty + netty-transport-native-epoll + ${netty.version} + linux-x86_64 + true + + + io.netty + netty-transport-native-kqueue + ${netty.version} + osx-x86_64 + true + + + junit + junit + ${junit.version} + + + + + + + org.casbin + jcasbin + true + + + io.lettuce + lettuce-core + true + + + org.apache.commons + commons-pool2 + true + + + org.apache.commons + commons-lang3 + true + + + org.slf4j + slf4j-api + true + + + junit + junit + test + + + + + + + + org.apache.maven.plugins + maven-gpg-plugin + ${maven-gpg-plugin.version} + + + sign-artifacts + verify + + sign + + + + + + + --pinentry-mode + loopback + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + ${maven-javadoc-plugin.version} + + ${java.version} + false + + + notnull + a + Not null + + + default + a + Default: + + + + + + + attach-javadocs + + jar + + + + + + + org.apache.maven.plugins + maven-source-plugin + ${maven-source-plugin.version} + + + attach-sources + + jar-no-fork + + + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + ${nexus-staging-maven-plugin.version} + true + + ossrh + https://oss.sonatype.org/ + + true + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + ${java.version} + ${java.version} + + + + org.codehaus.mojo + cobertura-maven-plugin + ${cobertura-maven-plugin.version} + + + html + xml + + + + + + + + \ No newline at end of file diff --git a/src/main/java/org/casbin/watcher/lettuce/LettuceRedisWatcher.java b/src/main/java/org/casbin/watcher/lettuce/LettuceRedisWatcher.java new file mode 100644 index 0000000..6c51ad0 --- /dev/null +++ b/src/main/java/org/casbin/watcher/lettuce/LettuceRedisWatcher.java @@ -0,0 +1,156 @@ +package org.casbin.watcher.lettuce; + +import io.lettuce.core.AbstractRedisClient; +import io.lettuce.core.ClientOptions; +import io.lettuce.core.RedisClient; +import io.lettuce.core.RedisURI; +import io.lettuce.core.cluster.ClusterClientOptions; +import io.lettuce.core.cluster.RedisClusterClient; +import io.lettuce.core.pubsub.StatefulRedisPubSubConnection; +import io.lettuce.core.resource.ClientResources; +import io.lettuce.core.resource.DefaultClientResources; +import org.apache.commons.lang3.StringUtils; +import org.casbin.jcasbin.persist.Watcher; +import org.casbin.watcher.lettuce.constants.WatcherConstant; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.UUID; +import java.util.function.Consumer; + +public class LettuceRedisWatcher implements Watcher { + private final String localId; + private final String redisChannelName; + private final AbstractRedisClient abstractRedisClient; + private LettuceSubThread lettuceSubThread; + private Runnable updateCallback; + + /** + * Constructor + * + * @param redisIp Redis IP + * @param redisPort Redis Port + * @param redisChannelName Redis Channel + * @param timeout Redis Timeout + * @param password Redis Password + * @param type Redis Type (standalone | cluster) + */ + public LettuceRedisWatcher(String redisIp, int redisPort, String redisChannelName, int timeout, String password, String type) { + this.abstractRedisClient = this.getLettuceRedisClient(redisIp, redisPort, password, timeout, type); + this.localId = UUID.randomUUID().toString(); + this.redisChannelName = redisChannelName; + this.startSub(); + } + + /** + * Constructor + * + * @param redisIp Redis IP + * @param redisPort Redis Port + * @param redisChannelName Redis Channel + * @param type Redis Type (standalone | cluster) + */ + public LettuceRedisWatcher(String redisIp, int redisPort, String redisChannelName, String type) { + this(redisIp, redisPort, redisChannelName, 2000, null, type); + } + + @Override + public void setUpdateCallback(Runnable runnable) { + this.updateCallback = runnable; + lettuceSubThread.setUpdateCallback(runnable); + } + + @Override + public void setUpdateCallback(Consumer consumer) { + this.lettuceSubThread.setUpdateCallback(consumer); + } + + @Override + public void update() { + try (StatefulRedisPubSubConnection statefulRedisPubSubConnection = + this.getStatefulRedisPubSubConnection(this.abstractRedisClient)) { + if (statefulRedisPubSubConnection.isOpen()) { + String msg = "Casbin policy has a new version from redis watcher: ".concat(this.localId); + statefulRedisPubSubConnection.async().publish(this.redisChannelName, msg); + } + } + } + + private void startSub() { + this.lettuceSubThread = new LettuceSubThread(this.abstractRedisClient, this.redisChannelName, this.updateCallback); + this.lettuceSubThread.start(); + } + + /** + * Initialize the Redis Client + * + * @param host Redis Host + * @param port Redis Port + * @param password Redis Password + * @param timeout Redis Timeout + * @param type Redis Type (standalone | cluster) default:standalone + * @return AbstractRedisClient + */ + private AbstractRedisClient getLettuceRedisClient(String host, int port, String password, int timeout, String type) { + // todo default standalone ? + // type = StringUtils.isEmpty(type) ? WatcherConstant.LETTUCE_REDIS_TYPE_STANDALONE : type; + if (StringUtils.isNotEmpty(type) && StringUtils.equalsAnyIgnoreCase(type, + WatcherConstant.LETTUCE_REDIS_TYPE_STANDALONE, WatcherConstant.LETTUCE_REDIS_TYPE_CLUSTER)) { + RedisURI redisUri = null; + if (StringUtils.isNotEmpty(password)) { + redisUri = RedisURI.builder() + .withHost(host) + .withPort(port) + .withPassword(password.toCharArray()) + .withTimeout(Duration.of(timeout, ChronoUnit.SECONDS)) + .build(); + } else { + redisUri = RedisURI.builder() + .withHost(host) + .withPort(port) + .withTimeout(Duration.of(timeout, ChronoUnit.SECONDS)) + .build(); + } + ClientResources clientResources = DefaultClientResources.builder() + .ioThreadPoolSize(4) + .computationThreadPoolSize(4) + .build(); + if (StringUtils.equalsIgnoreCase(type, WatcherConstant.LETTUCE_REDIS_TYPE_STANDALONE)) { + // standalone + ClientOptions clientOptions = ClientOptions.builder() + .autoReconnect(true) + .pingBeforeActivateConnection(true) + .build(); + RedisClient redisClient = RedisClient.create(clientResources, redisUri); + redisClient.setOptions(clientOptions); + return redisClient; + } else { + // cluster + ClusterClientOptions clusterClientOptions = ClusterClientOptions.builder() + .autoReconnect(true) + .pingBeforeActivateConnection(true) + .validateClusterNodeMembership(true) + .build(); + RedisClusterClient redisClusterClient = RedisClusterClient.create(clientResources, redisUri); + redisClusterClient.setOptions(clusterClientOptions); + return redisClusterClient; + } + } else { + throw new IllegalArgumentException("Redis-Type is required and can only be [standalone] or [cluster]"); + } + } + + /** + * Get Redis PubSub Connection + * + * @param abstractRedisClient Redis Client + * @return StatefulRedisPubSubConnection + */ + private StatefulRedisPubSubConnection getStatefulRedisPubSubConnection(AbstractRedisClient abstractRedisClient) { + if (abstractRedisClient instanceof RedisClient) { + return ((RedisClient) abstractRedisClient).connectPubSub(); + } else { + return ((RedisClusterClient) abstractRedisClient).connectPubSub(); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/casbin/watcher/lettuce/LettuceSubThread.java b/src/main/java/org/casbin/watcher/lettuce/LettuceSubThread.java new file mode 100644 index 0000000..62d1336 --- /dev/null +++ b/src/main/java/org/casbin/watcher/lettuce/LettuceSubThread.java @@ -0,0 +1,124 @@ +package org.casbin.watcher.lettuce; + +import io.lettuce.core.AbstractRedisClient; +import io.lettuce.core.RedisClient; +import io.lettuce.core.cluster.RedisClusterClient; +import io.lettuce.core.pubsub.RedisPubSubListener; +import io.lettuce.core.pubsub.StatefulRedisPubSubConnection; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.function.Consumer; + +public class LettuceSubThread extends Thread { + private static final Logger logger = LoggerFactory.getLogger(LettuceSubThread.class); + private final String channel; + private final LettuceSubscriber lettuceSubscriber; + private final AbstractRedisClient abstractRedisClient; + private StatefulRedisPubSubConnection statefulRedisPubSubConnection; + + /** + * Construction method + * + * @param abstractRedisClient abstractRedisClient + * @param channel channel + * @param updateCallback updateCallback + */ + public LettuceSubThread(AbstractRedisClient abstractRedisClient, String channel, Runnable updateCallback) { + super("LettuceSubThread"); + this.channel = channel; + this.abstractRedisClient = abstractRedisClient; + lettuceSubscriber = new LettuceSubscriber(updateCallback); + } + + /** + * set runnable + * + * @param runnable runnable + */ + public void setUpdateCallback(Runnable runnable) { + lettuceSubscriber.setUpdateCallback(runnable); + } + + /** + * set consumer + * + * @param consumer runnable + */ + public void setUpdateCallback(Consumer consumer) { + lettuceSubscriber.setUpdateCallback(consumer); + } + + @Override + public void run() { + try { + this.statefulRedisPubSubConnection = this.getStatefulRedisPubSubConnection(this.abstractRedisClient); + if (this.statefulRedisPubSubConnection.isOpen()) { + this.statefulRedisPubSubConnection.addListener(new RedisPubSubListener() { + @Override + public void unsubscribed(String channel, long count) { + logger.info("[unsubscribed] {}", channel); + } + + @Override + public void subscribed(String channel, long count) { + logger.info("[subscribed] {}", channel); + } + + @Override + public void punsubscribed(String pattern, long count) { + logger.info("[punsubscribed] {}", pattern); + } + + @Override + public void psubscribed(String pattern, long count) { + logger.info("[psubscribed] {}", pattern); + } + + @Override + public void message(String pattern, String channel, String message) { + logger.info("[message] {} -> {} -> {}", pattern, channel, message); + lettuceSubscriber.onMessage(channel, message); + } + + @Override + public void message(String channel, String message) { + logger.info("[message] {} -> {}", channel, message); + lettuceSubscriber.onMessage(channel, message); + } + }); + this.statefulRedisPubSubConnection.async().subscribe(this.channel); + + Thread.sleep(500); + } + } catch (Exception e) { + logger.error("error message {}", e.getMessage()); + this.close(this.statefulRedisPubSubConnection); + } + } + + /** + * Close Redis PubSub Connection + * + * @param statefulRedisPubSubConnection Redis PubSub Connection + */ + private void close(StatefulRedisPubSubConnection statefulRedisPubSubConnection) { + if (statefulRedisPubSubConnection.isOpen()) { + statefulRedisPubSubConnection.closeAsync(); + } + } + + /** + * Get Redis PubSub Connection + * + * @param abstractRedisClient Redis Client + * @return StatefulRedisPubSubConnection + */ + private StatefulRedisPubSubConnection getStatefulRedisPubSubConnection(AbstractRedisClient abstractRedisClient) { + if (abstractRedisClient instanceof RedisClient) { + return ((RedisClient) abstractRedisClient).connectPubSub(); + } else { + return ((RedisClusterClient) abstractRedisClient).connectPubSub(); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/casbin/watcher/lettuce/LettuceSubscriber.java b/src/main/java/org/casbin/watcher/lettuce/LettuceSubscriber.java new file mode 100644 index 0000000..286a032 --- /dev/null +++ b/src/main/java/org/casbin/watcher/lettuce/LettuceSubscriber.java @@ -0,0 +1,27 @@ +package org.casbin.watcher.lettuce; + +import java.util.function.Consumer; + +public class LettuceSubscriber { + private Runnable runnable; + private Consumer consumer; + + public LettuceSubscriber(Runnable updateCallback) { + this.runnable = updateCallback; + } + + public void setUpdateCallback(Runnable runnable){ + this.runnable = runnable; + } + + public void setUpdateCallback(Consumer consumer) { + this.consumer = consumer; + } + + public void onMessage(String channel, String message) { + runnable.run(); + if (this.consumer != null) { + this.consumer.accept("Channel: " + channel + " Message: " + message); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/casbin/watcher/lettuce/constants/WatcherConstant.java b/src/main/java/org/casbin/watcher/lettuce/constants/WatcherConstant.java new file mode 100644 index 0000000..0875c08 --- /dev/null +++ b/src/main/java/org/casbin/watcher/lettuce/constants/WatcherConstant.java @@ -0,0 +1,19 @@ +package org.casbin.watcher.lettuce.constants; + +/** + * Created by IntelliJ IDEA 2023. + * FileName: WatcherConstant.java + * + * @author shingmoyeung + * @since 2023/8/5 21:02 + * @version 1.0 + * To change this template use File Or Preferences | Settings | Editor | File and Code Templates. + * File Description: Redis Watcher Constant + */ +public class WatcherConstant { + /** + * Redis Type + */ + public static final String LETTUCE_REDIS_TYPE_STANDALONE = "standalone"; + public static final String LETTUCE_REDIS_TYPE_CLUSTER = "cluster"; +} \ No newline at end of file diff --git a/src/test/java/org/casbin/test/LettuceRedisWatcherTest.java b/src/test/java/org/casbin/test/LettuceRedisWatcherTest.java new file mode 100644 index 0000000..56ea5a2 --- /dev/null +++ b/src/test/java/org/casbin/test/LettuceRedisWatcherTest.java @@ -0,0 +1,63 @@ +package org.casbin.test; + +import org.casbin.jcasbin.main.Enforcer; +import org.casbin.watcher.lettuce.LettuceRedisWatcher; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class LettuceRedisWatcherTest { + /** + * LettuceRedisWatcher + */ + private LettuceRedisWatcher lettuceRedisWatcher; + + /** + * You should replace the initWatcher() method's content with your own Redis instance. + */ + @Before + public void initWatcher() { + String redisTopic = "jcasbin-topic"; + this.lettuceRedisWatcher = new LettuceRedisWatcher("127.0.0.1", 6379, redisTopic, 2000, "foobared", "standalone"); + Enforcer enforcer = new Enforcer(); + enforcer.setWatcher(this.lettuceRedisWatcher); + } + + @Test + public void testUpdate() throws InterruptedException { + this.initWatcher(); + this.lettuceRedisWatcher.update(); + Thread.sleep(100); + } + + @Test + public void testConsumerCallback() throws InterruptedException { + this.initWatcher(); + while (true) { + this.lettuceRedisWatcher.setUpdateCallback((s) -> System.out.println(s)); + this.lettuceRedisWatcher.update(); + Thread.sleep(500); + } + } + + @Test + public void testConnectWatcherWithoutPassword() { + String redisTopic = "jcasbin-topic"; + LettuceRedisWatcher lettuceRedisWatcherWithoutPassword = new LettuceRedisWatcher("127.0.0.1", 6378, redisTopic, "standalone"); + Assert.assertNotNull(lettuceRedisWatcherWithoutPassword); + } + + @Test + public void testConnectWatcherWithType() { + String redisTopic = "jcasbin-topic"; + Assert.assertThrows(IllegalArgumentException.class, () -> { + new LettuceRedisWatcher("192.168.3.244", 6379, redisTopic, "sentinel"); + }); + + LettuceRedisWatcher lettuceRedisWatcherStandalone = new LettuceRedisWatcher("192.168.3.244", 6378, redisTopic, "standalone"); + Assert.assertNotNull(lettuceRedisWatcherStandalone); + + LettuceRedisWatcher lettuceRedisWatcherCluster = new LettuceRedisWatcher("192.168.3.244", 6378, redisTopic, "cluster"); + Assert.assertNotNull(lettuceRedisWatcherCluster); + } +} \ No newline at end of file From 5569adf63e00e16d60d60aa75580df8d4292965e Mon Sep 17 00:00:00 2001 From: shingmoyeung <525032303@qq.com> Date: Mon, 7 Aug 2023 14:17:24 +0800 Subject: [PATCH 2/7] Refactor LettuceRedisWatcherTest.java to use local IP address instead of remote IP address for testing. --- src/test/java/org/casbin/test/LettuceRedisWatcherTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/java/org/casbin/test/LettuceRedisWatcherTest.java b/src/test/java/org/casbin/test/LettuceRedisWatcherTest.java index 56ea5a2..cf56ff2 100644 --- a/src/test/java/org/casbin/test/LettuceRedisWatcherTest.java +++ b/src/test/java/org/casbin/test/LettuceRedisWatcherTest.java @@ -51,13 +51,13 @@ public void testConnectWatcherWithoutPassword() { public void testConnectWatcherWithType() { String redisTopic = "jcasbin-topic"; Assert.assertThrows(IllegalArgumentException.class, () -> { - new LettuceRedisWatcher("192.168.3.244", 6379, redisTopic, "sentinel"); + new LettuceRedisWatcher("127.0.0.1", 6378, redisTopic, "sentinel"); }); - LettuceRedisWatcher lettuceRedisWatcherStandalone = new LettuceRedisWatcher("192.168.3.244", 6378, redisTopic, "standalone"); + LettuceRedisWatcher lettuceRedisWatcherStandalone = new LettuceRedisWatcher("127.0.0.1", 6378, redisTopic, "standalone"); Assert.assertNotNull(lettuceRedisWatcherStandalone); - LettuceRedisWatcher lettuceRedisWatcherCluster = new LettuceRedisWatcher("192.168.3.244", 6378, redisTopic, "cluster"); + LettuceRedisWatcher lettuceRedisWatcherCluster = new LettuceRedisWatcher("127.0.0.1", 6378, redisTopic, "cluster"); Assert.assertNotNull(lettuceRedisWatcherCluster); } } \ No newline at end of file From 0d0a7641bfd603685c416cd86b45521cb84cb3a6 Mon Sep 17 00:00:00 2001 From: shingmoyeung <525032303@qq.com> Date: Tue, 8 Aug 2023 08:05:32 +0800 Subject: [PATCH 3/7] Update LettuceRedisWatcherTest.java to simplify watcher initialization and update logic. --- .../java/org/casbin/test/LettuceRedisWatcherTest.java | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/test/java/org/casbin/test/LettuceRedisWatcherTest.java b/src/test/java/org/casbin/test/LettuceRedisWatcherTest.java index cf56ff2..bc149bd 100644 --- a/src/test/java/org/casbin/test/LettuceRedisWatcherTest.java +++ b/src/test/java/org/casbin/test/LettuceRedisWatcherTest.java @@ -25,19 +25,15 @@ public void initWatcher() { @Test public void testUpdate() throws InterruptedException { - this.initWatcher(); this.lettuceRedisWatcher.update(); Thread.sleep(100); } @Test public void testConsumerCallback() throws InterruptedException { - this.initWatcher(); - while (true) { - this.lettuceRedisWatcher.setUpdateCallback((s) -> System.out.println(s)); - this.lettuceRedisWatcher.update(); - Thread.sleep(500); - } + this.lettuceRedisWatcher.setUpdateCallback((s) -> System.out.println(s)); + this.lettuceRedisWatcher.update(); + Thread.sleep(500); } @Test From 3adb689bd905c205bd0c92d27b3205490b7d42c2 Mon Sep 17 00:00:00 2001 From: shingmoyeung <525032303@qq.com> Date: Tue, 8 Aug 2023 13:36:46 +0800 Subject: [PATCH 4/7] Refactor Redis connection initialization and add support for Redis Cluster. - Add Redis URI constants. - Update Redis connection initialization in LettuceRedisWatcher. - Add constructors for different Redis connection types. - Add support for Redis Cluster in LettuceRedisWatcher. - Update LettuceRedisWatcherTest to test Redis Cluster connection. --- .../watcher/lettuce/LettuceRedisWatcher.java | 86 +++++++++++++------ .../lettuce/constants/WatcherConstant.java | 6 ++ .../casbin/test/LettuceRedisWatcherTest.java | 29 ++++--- 3 files changed, 82 insertions(+), 39 deletions(-) diff --git a/src/main/java/org/casbin/watcher/lettuce/LettuceRedisWatcher.java b/src/main/java/org/casbin/watcher/lettuce/LettuceRedisWatcher.java index 6c51ad0..2776049 100644 --- a/src/main/java/org/casbin/watcher/lettuce/LettuceRedisWatcher.java +++ b/src/main/java/org/casbin/watcher/lettuce/LettuceRedisWatcher.java @@ -1,24 +1,28 @@ package org.casbin.watcher.lettuce; -import io.lettuce.core.AbstractRedisClient; -import io.lettuce.core.ClientOptions; -import io.lettuce.core.RedisClient; -import io.lettuce.core.RedisURI; +import io.lettuce.core.*; import io.lettuce.core.cluster.ClusterClientOptions; +import io.lettuce.core.cluster.ClusterTopologyRefreshOptions; import io.lettuce.core.cluster.RedisClusterClient; +import io.lettuce.core.cluster.RedisClusterURIUtil; import io.lettuce.core.pubsub.StatefulRedisPubSubConnection; import io.lettuce.core.resource.ClientResources; import io.lettuce.core.resource.DefaultClientResources; import org.apache.commons.lang3.StringUtils; import org.casbin.jcasbin.persist.Watcher; import org.casbin.watcher.lettuce.constants.WatcherConstant; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.net.URI; import java.time.Duration; import java.time.temporal.ChronoUnit; +import java.util.List; import java.util.UUID; import java.util.function.Consumer; public class LettuceRedisWatcher implements Watcher { + private static final Logger logger = LoggerFactory.getLogger(LettuceRedisWatcher.class); private final String localId; private final String redisChannelName; private final AbstractRedisClient abstractRedisClient; @@ -33,10 +37,9 @@ public class LettuceRedisWatcher implements Watcher { * @param redisChannelName Redis Channel * @param timeout Redis Timeout * @param password Redis Password - * @param type Redis Type (standalone | cluster) */ - public LettuceRedisWatcher(String redisIp, int redisPort, String redisChannelName, int timeout, String password, String type) { - this.abstractRedisClient = this.getLettuceRedisClient(redisIp, redisPort, password, timeout, type); + public LettuceRedisWatcher(String redisIp, Integer redisPort, String redisChannelName, int timeout, String password) { + this.abstractRedisClient = this.getLettuceRedisClient(redisIp, redisPort, null, password, timeout, WatcherConstant.LETTUCE_REDIS_TYPE_STANDALONE); this.localId = UUID.randomUUID().toString(); this.redisChannelName = redisChannelName; this.startSub(); @@ -48,10 +51,24 @@ public LettuceRedisWatcher(String redisIp, int redisPort, String redisChannelNam * @param redisIp Redis IP * @param redisPort Redis Port * @param redisChannelName Redis Channel - * @param type Redis Type (standalone | cluster) */ - public LettuceRedisWatcher(String redisIp, int redisPort, String redisChannelName, String type) { - this(redisIp, redisPort, redisChannelName, 2000, null, type); + public LettuceRedisWatcher(String redisIp, Integer redisPort, String redisChannelName) { + this(redisIp, redisPort, redisChannelName, 2000, null); + } + + /** + * Constructor + * + * @param nodes Redis Nodes + * @param redisChannelName Redis Channel + * @param timeout Redis Timeout + * @param password Redis Password + */ + public LettuceRedisWatcher(String nodes, String redisChannelName, Integer timeout, String password) { + this.abstractRedisClient = this.getLettuceRedisClient(null, null, nodes, password, timeout, WatcherConstant.LETTUCE_REDIS_TYPE_CLUSTER); + this.localId = UUID.randomUUID().toString(); + this.redisChannelName = redisChannelName; + this.startSub(); } @Override @@ -86,37 +103,38 @@ private void startSub() { * * @param host Redis Host * @param port Redis Port + * @param nodes Redis Nodes * @param password Redis Password * @param timeout Redis Timeout * @param type Redis Type (standalone | cluster) default:standalone * @return AbstractRedisClient */ - private AbstractRedisClient getLettuceRedisClient(String host, int port, String password, int timeout, String type) { + private AbstractRedisClient getLettuceRedisClient(String host, Integer port, String nodes, String password, int timeout, String type) { // todo default standalone ? // type = StringUtils.isEmpty(type) ? WatcherConstant.LETTUCE_REDIS_TYPE_STANDALONE : type; if (StringUtils.isNotEmpty(type) && StringUtils.equalsAnyIgnoreCase(type, WatcherConstant.LETTUCE_REDIS_TYPE_STANDALONE, WatcherConstant.LETTUCE_REDIS_TYPE_CLUSTER)) { - RedisURI redisUri = null; - if (StringUtils.isNotEmpty(password)) { - redisUri = RedisURI.builder() - .withHost(host) - .withPort(port) - .withPassword(password.toCharArray()) - .withTimeout(Duration.of(timeout, ChronoUnit.SECONDS)) - .build(); - } else { - redisUri = RedisURI.builder() - .withHost(host) - .withPort(port) - .withTimeout(Duration.of(timeout, ChronoUnit.SECONDS)) - .build(); - } ClientResources clientResources = DefaultClientResources.builder() .ioThreadPoolSize(4) .computationThreadPoolSize(4) .build(); if (StringUtils.equalsIgnoreCase(type, WatcherConstant.LETTUCE_REDIS_TYPE_STANDALONE)) { // standalone + RedisURI redisUri = null; + if (StringUtils.isNotEmpty(password)) { + redisUri = RedisURI.builder() + .withHost(host) + .withPort(port) + .withPassword(password.toCharArray()) + .withTimeout(Duration.of(timeout, ChronoUnit.SECONDS)) + .build(); + } else { + redisUri = RedisURI.builder() + .withHost(host) + .withPort(port) + .withTimeout(Duration.of(timeout, ChronoUnit.SECONDS)) + .build(); + } ClientOptions clientOptions = ClientOptions.builder() .autoReconnect(true) .pingBeforeActivateConnection(true) @@ -126,12 +144,26 @@ private AbstractRedisClient getLettuceRedisClient(String host, int port, String return redisClient; } else { // cluster + TimeoutOptions timeoutOptions = TimeoutOptions.builder().fixedTimeout(Duration.of(timeout, ChronoUnit.SECONDS)).build(); + ClusterTopologyRefreshOptions topologyRefreshOptions = ClusterTopologyRefreshOptions.builder() + .enablePeriodicRefresh(Duration.of(10, ChronoUnit.MINUTES)) + .enableAdaptiveRefreshTrigger(ClusterTopologyRefreshOptions.RefreshTrigger.MOVED_REDIRECT, ClusterTopologyRefreshOptions.RefreshTrigger.PERSISTENT_RECONNECTS) + .adaptiveRefreshTriggersTimeout(Duration.of(30, ChronoUnit.SECONDS)) + .build(); ClusterClientOptions clusterClientOptions = ClusterClientOptions.builder() .autoReconnect(true) + .timeoutOptions(timeoutOptions) + .topologyRefreshOptions(topologyRefreshOptions) .pingBeforeActivateConnection(true) .validateClusterNodeMembership(true) .build(); - RedisClusterClient redisClusterClient = RedisClusterClient.create(clientResources, redisUri); + // Redis Cluster Node + String redisUri = StringUtils.isNotEmpty(password) ? + WatcherConstant.REDIS_URI_PREFIX.concat(password).concat(WatcherConstant.REDIS_URI_PASSWORD_SPLIT).concat(nodes) : + WatcherConstant.REDIS_URI_PREFIX.concat(nodes); + logger.info("Redis Cluster Uri: {}", redisUri); + List redisURIList = RedisClusterURIUtil.toRedisURIs(URI.create(redisUri)); + RedisClusterClient redisClusterClient = RedisClusterClient.create(clientResources, redisURIList); redisClusterClient.setOptions(clusterClientOptions); return redisClusterClient; } diff --git a/src/main/java/org/casbin/watcher/lettuce/constants/WatcherConstant.java b/src/main/java/org/casbin/watcher/lettuce/constants/WatcherConstant.java index 0875c08..d0568b1 100644 --- a/src/main/java/org/casbin/watcher/lettuce/constants/WatcherConstant.java +++ b/src/main/java/org/casbin/watcher/lettuce/constants/WatcherConstant.java @@ -16,4 +16,10 @@ public class WatcherConstant { */ public static final String LETTUCE_REDIS_TYPE_STANDALONE = "standalone"; public static final String LETTUCE_REDIS_TYPE_CLUSTER = "cluster"; + + /** + * Redis URI + */ + public static final String REDIS_URI_PREFIX = "redis://"; + public static final String REDIS_URI_PASSWORD_SPLIT = "@"; } \ No newline at end of file diff --git a/src/test/java/org/casbin/test/LettuceRedisWatcherTest.java b/src/test/java/org/casbin/test/LettuceRedisWatcherTest.java index bc149bd..601d209 100644 --- a/src/test/java/org/casbin/test/LettuceRedisWatcherTest.java +++ b/src/test/java/org/casbin/test/LettuceRedisWatcherTest.java @@ -18,42 +18,47 @@ public class LettuceRedisWatcherTest { @Before public void initWatcher() { String redisTopic = "jcasbin-topic"; - this.lettuceRedisWatcher = new LettuceRedisWatcher("127.0.0.1", 6379, redisTopic, 2000, "foobared", "standalone"); + this.lettuceRedisWatcher = new LettuceRedisWatcher("192.168.3.244", 6379, redisTopic, 2000, "123456"); + Enforcer enforcer = new Enforcer(); + enforcer.setWatcher(this.lettuceRedisWatcher); + } + + public void initClusterWatcher() { + String redisTopic = "jcasbin-topic"; + // modify your cluster nodes + this.lettuceRedisWatcher = new LettuceRedisWatcher("192.168.1.234:6380,192.168.1.234:6381,192.168.1.234:6382", redisTopic, 2000, "123456"); Enforcer enforcer = new Enforcer(); enforcer.setWatcher(this.lettuceRedisWatcher); } @Test public void testUpdate() throws InterruptedException { + // this.initClusterWatcher(); this.lettuceRedisWatcher.update(); Thread.sleep(100); } @Test public void testConsumerCallback() throws InterruptedException { + // this.initClusterWatcher(); + // while (true) { this.lettuceRedisWatcher.setUpdateCallback((s) -> System.out.println(s)); this.lettuceRedisWatcher.update(); - Thread.sleep(500); + Thread.sleep(100); + // } } @Test public void testConnectWatcherWithoutPassword() { String redisTopic = "jcasbin-topic"; - LettuceRedisWatcher lettuceRedisWatcherWithoutPassword = new LettuceRedisWatcher("127.0.0.1", 6378, redisTopic, "standalone"); + LettuceRedisWatcher lettuceRedisWatcherWithoutPassword = new LettuceRedisWatcher("127.0.0.1", 6378, redisTopic); Assert.assertNotNull(lettuceRedisWatcherWithoutPassword); } @Test - public void testConnectWatcherWithType() { + public void testConnectWatcherCluster() { String redisTopic = "jcasbin-topic"; - Assert.assertThrows(IllegalArgumentException.class, () -> { - new LettuceRedisWatcher("127.0.0.1", 6378, redisTopic, "sentinel"); - }); - - LettuceRedisWatcher lettuceRedisWatcherStandalone = new LettuceRedisWatcher("127.0.0.1", 6378, redisTopic, "standalone"); - Assert.assertNotNull(lettuceRedisWatcherStandalone); - - LettuceRedisWatcher lettuceRedisWatcherCluster = new LettuceRedisWatcher("127.0.0.1", 6378, redisTopic, "cluster"); + LettuceRedisWatcher lettuceRedisWatcherCluster = new LettuceRedisWatcher("127.0.0.1:6380,127.0.0.1:6381,127.0.0.1:6382", redisTopic, 2000, null); Assert.assertNotNull(lettuceRedisWatcherCluster); } } \ No newline at end of file From 28b8232606cfd3517df9d6bc414063bce67852ea Mon Sep 17 00:00:00 2001 From: shingmoyeung <525032303@qq.com> Date: Tue, 8 Aug 2023 13:39:04 +0800 Subject: [PATCH 5/7] Update LettuceRedisWatcherTest.java to use localhost as the redis server instead of the specific IP address. --- src/test/java/org/casbin/test/LettuceRedisWatcherTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/org/casbin/test/LettuceRedisWatcherTest.java b/src/test/java/org/casbin/test/LettuceRedisWatcherTest.java index 601d209..71ee15d 100644 --- a/src/test/java/org/casbin/test/LettuceRedisWatcherTest.java +++ b/src/test/java/org/casbin/test/LettuceRedisWatcherTest.java @@ -18,7 +18,7 @@ public class LettuceRedisWatcherTest { @Before public void initWatcher() { String redisTopic = "jcasbin-topic"; - this.lettuceRedisWatcher = new LettuceRedisWatcher("192.168.3.244", 6379, redisTopic, 2000, "123456"); + this.lettuceRedisWatcher = new LettuceRedisWatcher("127.0.0.1", 6379, redisTopic, 2000, "foobared"); Enforcer enforcer = new Enforcer(); enforcer.setWatcher(this.lettuceRedisWatcher); } From a1f16af464d80f45d17d58c0a52afd70735ba8c7 Mon Sep 17 00:00:00 2001 From: shingmoyeung <525032303@qq.com> Date: Tue, 8 Aug 2023 14:28:39 +0800 Subject: [PATCH 6/7] Update node version from 16 to 18 in maven-ci.yml and add new semantic.yml. --- .github/semantic.yml | 5 +++++ .github/workflows/maven-ci.yml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .github/semantic.yml diff --git a/.github/semantic.yml b/.github/semantic.yml new file mode 100644 index 0000000..775c00d --- /dev/null +++ b/.github/semantic.yml @@ -0,0 +1,5 @@ +type: + - feat + +scope: + - add lettuce \ No newline at end of file diff --git a/.github/workflows/maven-ci.yml b/.github/workflows/maven-ci.yml index 12d6f9f..b0d1e8f 100644 --- a/.github/workflows/maven-ci.yml +++ b/.github/workflows/maven-ci.yml @@ -45,7 +45,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v2 with: - node-version: 16 + node-version: 18 - name: Sematic Release run: | From 01ea04d04bae136ea4c5c6ddb9dbfa4937390c32 Mon Sep 17 00:00:00 2001 From: shingmoyeung <525032303@qq.com> Date: Tue, 8 Aug 2023 14:35:19 +0800 Subject: [PATCH 7/7] Refactor semantic.yml to disable validation for the PR title and all the commits. --- .github/semantic.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/semantic.yml b/.github/semantic.yml index 775c00d..e500cff 100644 --- a/.github/semantic.yml +++ b/.github/semantic.yml @@ -1,5 +1,2 @@ -type: - - feat - -scope: - - add lettuce \ No newline at end of file +# Always validate the PR title AND all the commits +titleAndCommits: false \ No newline at end of file