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..cf56ff2 --- /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("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"); + Assert.assertNotNull(lettuceRedisWatcherCluster); + } +} \ No newline at end of file