From a1d0ff1a89a477e8685535f64430f369d638f772 Mon Sep 17 00:00:00 2001 From: Nikita Koksharov Date: Mon, 14 Jun 2021 10:08:14 +0300 Subject: [PATCH] Feature - Micronaut integration #3657 --- pom.xml | 1 + redisson-micronaut/README.md | 132 +++++ redisson-micronaut/pom.xml | 170 +++++++ .../micronaut/RedissonConfiguration.java | 113 +++++ .../redisson/micronaut/RedissonFactory.java | 68 +++ .../micronaut/cache/RedissonAsyncCache.java | 123 +++++ .../cache/RedissonCacheConfiguration.java | 110 ++++ .../micronaut/cache/RedissonSyncCache.java | 113 +++++ .../micronaut/session/AttributeMessage.java | 79 +++ .../session/AttributeRemoveMessage.java | 42 ++ .../session/AttributeUpdateMessage.java | 50 ++ .../session/AttributesClearMessage.java | 32 ++ .../session/AttributesPutAllMessage.java | 61 +++ .../RedissonHttpSessionConfiguration.java | 99 ++++ .../micronaut/session/RedissonSession.java | 283 +++++++++++ .../session/RedissonSessionStore.java | 276 ++++++++++ .../session/SessionCreatedMessage.java | 32 ++ .../session/SessionDestroyedMessage.java | 32 ++ .../redisson/native-image.properties | 1 + .../org.redisson/redisson/proxy-config.json | 91 ++++ .../org.redisson/redisson/reflect-config.json | 479 ++++++++++++++++++ .../redisson/resource-config.json | 8 + .../redisson/micronaut/RedissonCacheTest.java | 45 ++ .../micronaut/RedissonSessionTest.java | 202 ++++++++ 24 files changed, 2642 insertions(+) create mode 100644 redisson-micronaut/README.md create mode 100644 redisson-micronaut/pom.xml create mode 100644 redisson-micronaut/src/main/java/org/redisson/micronaut/RedissonConfiguration.java create mode 100644 redisson-micronaut/src/main/java/org/redisson/micronaut/RedissonFactory.java create mode 100644 redisson-micronaut/src/main/java/org/redisson/micronaut/cache/RedissonAsyncCache.java create mode 100644 redisson-micronaut/src/main/java/org/redisson/micronaut/cache/RedissonCacheConfiguration.java create mode 100644 redisson-micronaut/src/main/java/org/redisson/micronaut/cache/RedissonSyncCache.java create mode 100644 redisson-micronaut/src/main/java/org/redisson/micronaut/session/AttributeMessage.java create mode 100644 redisson-micronaut/src/main/java/org/redisson/micronaut/session/AttributeRemoveMessage.java create mode 100644 redisson-micronaut/src/main/java/org/redisson/micronaut/session/AttributeUpdateMessage.java create mode 100644 redisson-micronaut/src/main/java/org/redisson/micronaut/session/AttributesClearMessage.java create mode 100644 redisson-micronaut/src/main/java/org/redisson/micronaut/session/AttributesPutAllMessage.java create mode 100644 redisson-micronaut/src/main/java/org/redisson/micronaut/session/RedissonHttpSessionConfiguration.java create mode 100644 redisson-micronaut/src/main/java/org/redisson/micronaut/session/RedissonSession.java create mode 100644 redisson-micronaut/src/main/java/org/redisson/micronaut/session/RedissonSessionStore.java create mode 100644 redisson-micronaut/src/main/java/org/redisson/micronaut/session/SessionCreatedMessage.java create mode 100644 redisson-micronaut/src/main/java/org/redisson/micronaut/session/SessionDestroyedMessage.java create mode 100644 redisson-micronaut/src/main/resources/META-INF/native-image/org.redisson/redisson/native-image.properties create mode 100644 redisson-micronaut/src/main/resources/META-INF/native-image/org.redisson/redisson/proxy-config.json create mode 100644 redisson-micronaut/src/main/resources/META-INF/native-image/org.redisson/redisson/reflect-config.json create mode 100644 redisson-micronaut/src/main/resources/META-INF/native-image/org.redisson/redisson/resource-config.json create mode 100644 redisson-micronaut/src/test/java/org/redisson/micronaut/RedissonCacheTest.java create mode 100644 redisson-micronaut/src/test/java/org/redisson/micronaut/RedissonSessionTest.java diff --git a/pom.xml b/pom.xml index 1fcefc54c..7f278af74 100644 --- a/pom.xml +++ b/pom.xml @@ -62,6 +62,7 @@ redisson-hibernate redisson-helidon redisson-quarkus + redisson-micronaut diff --git a/redisson-micronaut/README.md b/redisson-micronaut/README.md new file mode 100644 index 000000000..381cf767b --- /dev/null +++ b/redisson-micronaut/README.md @@ -0,0 +1,132 @@ +# Redis integration with Micronaut + +Integrates Redisson with [Micronaut](https://micronaut.io/) framework. + +Supports Micronaut 2.0.x - 2.5.x + +## Usage + +### 1. Add `redisson-micronaut` dependency into your project: + +Maven + +```xml + + org.redisson + redisson-micronaut + 3.16.0 + +``` + +Gradle + +```groovy +compile 'org.redisson:redisson-micronaut:3.16.0' +``` + +### 2. Add settings into `application.yml` file + +#### 2.1 Redisson settings + +Config structure is a Redisson YAML configuration - +[single mode](https://github.com/redisson/redisson/wiki/2.-Configuration#262-single-instance-yaml-config-format), +[replicated mode](https://github.com/redisson/redisson/wiki/2.-Configuration#252-replicated-yaml-config-format), +[cluster mode](https://github.com/redisson/redisson/wiki/2.-Configuration#242-cluster-yaml-config-format), +[sentinel mode](https://github.com/redisson/redisson/wiki/2.-Configuration#272-sentinel-yaml-config-format), +[proxy mode](https://github.com/redisson/redisson/wiki/2.-Configuration#292-proxy-mode-yaml-config-format) + +NOTE: Setting names in camel case should be joined with hyphens (-). + +Config example: +```yaml +redisson: + single-server-config: + address: "redis://127.0.0.1:6379" + threads: 16 + netty-threads: 32 +``` + +#### 2.2 Cache settings + +|Setting name|Type|Description| +|------------|----|-----------| +|redisson.caches.*.max-size|java.lang.Integer|Max size of this cache. Superfluous elements are evicted using LRU algorithm. If 0 the cache is unbounded (default).| +|redisson.caches.*.codec|java.lang.Class|Redis data codec applied to cache entries. Default is MarshallingCodec codec.| +|redisson.caches.*.expire-after-write|java.time.Duration|Cache entry time to live duration applied after each write operation.| +|redisson.caches.*.expire-after-access|java.time.Duration|Cache entry time to live duration applied after each read operation.| + +Config example: +```yaml +redisson: + single-server-config: + address: "redis://127.0.0.1:6379" + caches: + my-cache1: + expire-after-write: 10s + expire-after-access: 3s + max-size: 1000 + codec: org.redisson.codec.MarshallingCodec + my-cache2: + expire-after-write: 200s + expire-after-access: 30s +``` + +#### 2.3 Session settings + +[Session](https://docs.micronaut.io/latest/api/io/micronaut/session/Session.html) store implementation. +Additional settings to [HttpSessionConfiguration](https://docs.micronaut.io/2.5.4/api/io/micronaut/session/http/HttpSessionConfiguration.html) object: + +|Setting name|Type|Description| +|------------|----|-----------| +|micronaut.session.http.redisson.enabled|java.lang.Boolean|Enables Session store| +|micronaut.session.http.redisson.key-prefix|java.lang.Integer|Defines string prefix applied to all objects stored in Redis.| +|micronaut.session.http.redisson.codec|java.lang.Class|Redis data codec applied to cache entries. Default is MarshallingCodec codec.| +|micronaut.session.http.redisson.update-mode|java.lang.String|Defines session attributes update mode.
`WRITE_BEHIND` - session changes stored asynchronously.
`AFTER_REQUEST` - session changes stored only on `SessionStore#save(Session)` method invocation. Default value.| +|micronaut.session.http.redisson.broadcastSessionUpdates|java.lang.Boolean|Defines broadcasting of session updates across all micronaut services.| + + +Config example: + +```yaml +micronaut: + session: + http: + redisson: + enabled: true + update-mode: "WRITE_BEHIND" + broadcast-session-updates: false +``` +### 3 Use Redisson + +#### 3.1 Redisson instance + +```java +@Inject +private RedissonClient redisson; +``` + +#### 3.2 Cache + +```java +@Singleton +@CacheConfig("my-cache1") +public class CarsService { + + @Cacheable + public List listAll() { + // ... + } + + @CachePut(parameters = {"type"}) + public List addCar(String type, String description) { + // ... + } + + @CacheInvalidate(parameters = {"type"}) + public void removeCar(String type, String description) { + // ... + } +} +``` + +Consider __[Redisson PRO](https://redisson.pro)__ version for **ultra-fast performance** and **support by SLA**. diff --git a/redisson-micronaut/pom.xml b/redisson-micronaut/pom.xml new file mode 100644 index 000000000..6f045b659 --- /dev/null +++ b/redisson-micronaut/pom.xml @@ -0,0 +1,170 @@ + + 4.0.0 + + + org.redisson + redisson-parent + 3.15.7-SNAPSHOT + ../ + + + redisson-micronaut + jar + + Redisson/Micronaut Cache and Integration + + + + + com.mycila + license-maven-plugin + 3.0 + + ${basedir} +
${basedir}/../header.txt
+ false + true + false + + src/main/java/org/redisson/ + + + target/** + + true + + JAVADOC_STYLE + + true + true + UTF-8 +
+ + + + check + + + +
+ + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + + -parameters + + + + io.micronaut + micronaut-inject-java + 2.0.0 + + + io.micronaut + micronaut-validation + 2.0.0 + + + + + + test-compile + + testCompile + + + + -parameters + + + + io.micronaut + micronaut-inject-java + 2.0.0 + + + io.micronaut + micronaut-validation + 2.0.0 + + + + + + + +
+
+ + + + io.micronaut.cache + micronaut-cache-core + 2.4.0 + true + + + + io.micronaut + micronaut-session + 2.5.5 + true + + + + io.micronaut + micronaut-management + 2.5.5 + true + + + + io.micronaut + micronaut-inject-java + 2.5.5 + test + + + + org.redisson + redisson + ${project.version} + + + + org.assertj + assertj-core + 3.18.1 + test + + + + org.junit.jupiter + junit-jupiter-engine + 5.7.1 + test + + + org.junit.jupiter + junit-jupiter-params + 5.7.1 + test + + + io.micronaut.test + micronaut-test-junit5 + [2.0.0,) + test + + + ch.qos.logback + logback-classic + 1.2.3 + test + + + +
diff --git a/redisson-micronaut/src/main/java/org/redisson/micronaut/RedissonConfiguration.java b/redisson-micronaut/src/main/java/org/redisson/micronaut/RedissonConfiguration.java new file mode 100644 index 000000000..31381d6aa --- /dev/null +++ b/redisson-micronaut/src/main/java/org/redisson/micronaut/RedissonConfiguration.java @@ -0,0 +1,113 @@ +/** + * Copyright (c) 2013-2021 Nikita Koksharov + * + * Licensed 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.redisson.micronaut; + +import io.micronaut.context.annotation.ConfigurationBuilder; +import io.micronaut.context.annotation.ConfigurationProperties; +import io.micronaut.context.annotation.Requires; +import org.redisson.config.*; + +/** + * + * @author Nikita Koksharov + * + */ +@ConfigurationProperties("redisson") +@Requires(missingBeans = Config.class) +@Requires(property = "redisson") +public class RedissonConfiguration extends Config { + + public RedissonConfiguration() { + } + + @Override + public SingleServerConfig getSingleServerConfig() { + if (isNotDefined()) { + return useSingleServer(); + } + return super.getSingleServerConfig(); + } + + @Override + @ConfigurationBuilder("singleServerConfig") + protected void setSingleServerConfig(SingleServerConfig singleConnectionConfig) { + super.setSingleServerConfig(singleConnectionConfig); + } + + @Override + public ClusterServersConfig getClusterServersConfig() { + if (isNotDefined()) { + return useClusterServers(); + } + return super.getClusterServersConfig(); + } + + @Override + @ConfigurationBuilder(value = "clusterServersConfig", includes = {"nodeAddresses"}) + protected void setClusterServersConfig(ClusterServersConfig clusterServersConfig) { + super.setClusterServersConfig(clusterServersConfig); + } + + private boolean isNotDefined() { + return super.getSingleServerConfig() == null + && super.getClusterServersConfig() == null + && super.getReplicatedServersConfig() == null + && super.getSentinelServersConfig() == null + && super.getMasterSlaveServersConfig() == null; + } + + @Override + public ReplicatedServersConfig getReplicatedServersConfig() { + if (isNotDefined()) { + return useReplicatedServers(); + } + return super.getReplicatedServersConfig(); + } + + @Override + @ConfigurationBuilder(value = "replicatedServersConfig", includes = {"nodeAddresses"}) + protected void setReplicatedServersConfig(ReplicatedServersConfig replicatedServersConfig) { + super.setReplicatedServersConfig(replicatedServersConfig); + } + + @Override + public SentinelServersConfig getSentinelServersConfig() { + if (isNotDefined()) { + return useSentinelServers(); + } + return super.getSentinelServersConfig(); + } + + @Override + @ConfigurationBuilder(value = "sentinelServersConfig", includes = {"sentinelAddresses"}) + protected void setSentinelServersConfig(SentinelServersConfig sentinelConnectionConfig) { + super.setSentinelServersConfig(sentinelConnectionConfig); + } + + @Override + public MasterSlaveServersConfig getMasterSlaveServersConfig() { + if (isNotDefined()) { + return useMasterSlaveServers(); + } + return super.getMasterSlaveServersConfig(); + } + + @Override + @ConfigurationBuilder(value = "masterSlaveServersConfig", includes = {"slaveAddresses"}) + protected void setMasterSlaveServersConfig(MasterSlaveServersConfig masterSlaveConnectionConfig) { + super.setMasterSlaveServersConfig(masterSlaveConnectionConfig); + } +} diff --git a/redisson-micronaut/src/main/java/org/redisson/micronaut/RedissonFactory.java b/redisson-micronaut/src/main/java/org/redisson/micronaut/RedissonFactory.java new file mode 100644 index 000000000..98bf5026b --- /dev/null +++ b/redisson-micronaut/src/main/java/org/redisson/micronaut/RedissonFactory.java @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2013-2021 Nikita Koksharov + * + * Licensed 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.redisson.micronaut; + +import io.micronaut.context.annotation.*; +import io.micronaut.core.convert.ConversionService; +import io.micronaut.scheduling.TaskExecutors; +import org.redisson.Redisson; +import org.redisson.api.RMap; +import org.redisson.api.RMapCache; +import org.redisson.api.RedissonClient; +import org.redisson.client.codec.Codec; +import org.redisson.config.Config; +import org.redisson.micronaut.cache.RedissonSyncCache; +import org.redisson.micronaut.cache.RedissonCacheConfiguration; + +import javax.inject.Named; +import javax.inject.Singleton; +import java.util.Optional; +import java.util.concurrent.ExecutorService; + +/** + * + * @author Nikita Koksharov + * + */ +@Factory +public class RedissonFactory { + + @Requires(beans = Config.class) + @Singleton + @Bean(preDestroy = "shutdown") + public RedissonClient redisson(Config config) { + return Redisson.create(config); + } + + @EachBean(RedissonCacheConfiguration.class) + public RedissonSyncCache cache(@Parameter RedissonCacheConfiguration configuration, + RedissonClient redisson, + ConversionService conversionService, + @Named(TaskExecutors.IO) ExecutorService executorService) { + Codec codec = Optional.ofNullable(configuration.getCodec()) + .orElse(redisson.getConfig().getCodec()); + if (configuration.getExpireAfterAccess().toMillis() != 0 + || configuration.getExpireAfterWrite().toMillis() != 0 + || configuration.getMaxSize() != 0) { + RMapCache mapCache = redisson.getMapCache(configuration.getName(), codec); + return new RedissonSyncCache(conversionService, mapCache, mapCache, executorService, configuration); + } + RMap map = redisson.getMap(configuration.getName(), codec); + return new RedissonSyncCache(conversionService, null, map, executorService, configuration); + } + + +} diff --git a/redisson-micronaut/src/main/java/org/redisson/micronaut/cache/RedissonAsyncCache.java b/redisson-micronaut/src/main/java/org/redisson/micronaut/cache/RedissonAsyncCache.java new file mode 100644 index 000000000..05e00fb2f --- /dev/null +++ b/redisson-micronaut/src/main/java/org/redisson/micronaut/cache/RedissonAsyncCache.java @@ -0,0 +1,123 @@ +/** + * Copyright (c) 2013-2021 Nikita Koksharov + * + * Licensed 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.redisson.micronaut.cache; + +import io.micronaut.cache.AsyncCache; +import io.micronaut.core.convert.ConversionContext; +import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.type.Argument; +import io.micronaut.core.util.ArgumentUtils; +import org.redisson.api.RMap; +import org.redisson.api.RMapCache; + +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.function.Supplier; + +/** + * + * @author Nikita Koksharov + * + */ +public class RedissonAsyncCache implements AsyncCache> { + + private final ConversionService conversionService; + private final RMapCache mapCache; + private final RMap map; + private final ExecutorService executorService; + + public RedissonAsyncCache(RMapCache mapCache, + RMap map, + ExecutorService executorService, + ConversionService conversionService) { + this.mapCache = mapCache; + this.map = map; + this.executorService = executorService; + this.conversionService = conversionService; + } + + @Override + public CompletableFuture> get(Object key, Argument requiredType) { + ArgumentUtils.requireNonNull("key", key); + return map.getAsync(key) + .thenApply(v -> { + if (v != null) { + return Optional.of((T)conversionService.convert(v, ConversionContext.of(requiredType))); + } + return Optional.empty(); + }) + .toCompletableFuture(); + } + + @Override + public CompletableFuture get(Object key, Argument requiredType, Supplier supplier) { + ArgumentUtils.requireNonNull("key", key); + return get(key, requiredType).thenCompose(existingValue -> { + if (existingValue.isPresent()) { + return CompletableFuture.completedFuture(existingValue.get()); + } else { + return CompletableFuture.supplyAsync(supplier, executorService) + .thenApply(value -> { + put(key, value); + return value; + }); + } + }); + } + + @Override + public CompletableFuture> putIfAbsent(Object key, T value) { + ArgumentUtils.requireNonNull("key", key); + ArgumentUtils.requireNonNull("value", value); + return map.putIfAbsentAsync(key, value) + .thenApply(v -> Optional.ofNullable((T) v)) + .toCompletableFuture(); + } + + @Override + public CompletableFuture put(Object key, Object value) { + ArgumentUtils.requireNonNull("key", key); + ArgumentUtils.requireNonNull("value", value); + return map.fastPutAsync(key, value) + .thenApply(counter -> true) + .toCompletableFuture(); + } + + @Override + public CompletableFuture invalidate(Object key) { + ArgumentUtils.requireNonNull("key", key); + return map.fastRemoveAsync(key) + .thenApply(counter -> true) + .toCompletableFuture(); + } + + @Override + public CompletableFuture invalidateAll() { + return map.deleteAsync() + .toCompletableFuture(); + } + + @Override + public String getName() { + return map.getName(); + } + + @Override + public RMap getNativeCache() { + return map; + } +} diff --git a/redisson-micronaut/src/main/java/org/redisson/micronaut/cache/RedissonCacheConfiguration.java b/redisson-micronaut/src/main/java/org/redisson/micronaut/cache/RedissonCacheConfiguration.java new file mode 100644 index 000000000..965c624d8 --- /dev/null +++ b/redisson-micronaut/src/main/java/org/redisson/micronaut/cache/RedissonCacheConfiguration.java @@ -0,0 +1,110 @@ +/** + * Copyright (c) 2013-2021 Nikita Koksharov + * + * Licensed 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.redisson.micronaut.cache; + +import edu.umd.cs.findbugs.annotations.NonNull; +import io.micronaut.context.annotation.EachProperty; +import io.micronaut.context.annotation.Parameter; +import io.micronaut.core.naming.Named; +import org.redisson.client.codec.Codec; + +import java.time.Duration; + +/** + * Micronaut Cache settings. + * + * @author Nikita Koksharov + * + */ +@EachProperty("redisson.caches") +public class RedissonCacheConfiguration implements Named { + + private final String name; + + private Codec codec; + private Duration expireAfterWrite; + private Duration expireAfterAccess; + private int maxSize; + + public RedissonCacheConfiguration(@Parameter String name) { + this.name = name; + } + + @NonNull + @Override + public String getName() { + return name; + } + + public Codec getCodec() { + return codec; + } + + /** + * Redis data codec applied to cache entries. + * Default is MarshallingCodec codec + * + * @see org.redisson.client.codec.Codec + * @see org.redisson.codec.MarshallingCodec + * + * @param codec - data codec + * @return config + */ + public void setCodec(Codec codec) { + this.codec = codec; + } + + public Duration getExpireAfterWrite() { + return expireAfterWrite; + } + + /** + * Cache entry time to live duration applied after each write operation. + * + * @param expireAfterWrite - time to live duration + */ + public void setExpireAfterWrite(Duration expireAfterWrite) { + this.expireAfterWrite = expireAfterWrite; + } + + public Duration getExpireAfterAccess() { + return expireAfterAccess; + } + + /** + * Cache entry time to live duration applied after each read operation. + * + * @param expireAfterAccess - time to live duration + */ + public void setExpireAfterAccess(Duration expireAfterAccess) { + this.expireAfterAccess = expireAfterAccess; + } + + public int getMaxSize() { + return maxSize; + } + + /** + * Max size of this cache. Superfluous elements are evicted using LRU algorithm. + * + * @param maxSize - max size + * If 0 the cache is unbounded (default). + */ + public void setMaxSize(int maxSize) { + this.maxSize = maxSize; + } + +} diff --git a/redisson-micronaut/src/main/java/org/redisson/micronaut/cache/RedissonSyncCache.java b/redisson-micronaut/src/main/java/org/redisson/micronaut/cache/RedissonSyncCache.java new file mode 100644 index 000000000..9c7805600 --- /dev/null +++ b/redisson-micronaut/src/main/java/org/redisson/micronaut/cache/RedissonSyncCache.java @@ -0,0 +1,113 @@ +/** + * Copyright (c) 2013-2021 Nikita Koksharov + * + * Licensed 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.redisson.micronaut.cache; + +import edu.umd.cs.findbugs.annotations.NonNull; +import io.micronaut.cache.AbstractMapBasedSyncCache; +import io.micronaut.cache.AsyncCache; +import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.util.ArgumentUtils; +import org.redisson.api.RMap; +import org.redisson.api.RMapCache; + +import java.util.Optional; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +/** + * + * @author Nikita Koksharov + * + */ +public class RedissonSyncCache extends AbstractMapBasedSyncCache> { + + private final ConversionService conversionService; + private final ExecutorService executorService; + private final RedissonCacheConfiguration configuration; + private final RMapCache mapCache; + private final RMap map; + + public RedissonSyncCache(ConversionService conversionService, + RMapCache mapCache, + RMap map, + ExecutorService executorService, + RedissonCacheConfiguration configuration) { + super(conversionService, map); + this.executorService = executorService; + this.configuration = configuration; + this.mapCache = mapCache; + this.map = map; + this.conversionService = conversionService; + if (configuration.getMaxSize() != 0) { + mapCache.setMaxSize(configuration.getMaxSize()); + } + } + + @Override + public String getName() { + return getNativeCache().getName(); + } + + @NonNull + @Override + public Optional putIfAbsent(@NonNull Object key, @NonNull T value) { + ArgumentUtils.requireNonNull("key", key); + ArgumentUtils.requireNonNull("value", value); + T res; + if (mapCache != null) { + res = (T) mapCache.putIfAbsent(key, value, configuration.getExpireAfterWrite().toMillis(), TimeUnit.MILLISECONDS, + configuration.getExpireAfterAccess().toMillis(), TimeUnit.MILLISECONDS); + } else { + res = (T) mapCache.putIfAbsent(key, value); + } + return Optional.ofNullable(res); + } + + @NonNull + @Override + public T putIfAbsent(@NonNull Object key, @NonNull Supplier value) { + ArgumentUtils.requireNonNull("key", key); + ArgumentUtils.requireNonNull("value", value); + T val = value.get(); + T res; + if (mapCache != null) { + res = (T) mapCache.putIfAbsent(key, val, configuration.getExpireAfterWrite().toMillis(), TimeUnit.MILLISECONDS, + configuration.getExpireAfterAccess().toMillis(), TimeUnit.MILLISECONDS); + } else { + res = (T) mapCache.putIfAbsent(key, value); + } + return Optional.ofNullable(res).orElse(val); + } + + @Override + public void put(@NonNull Object key, @NonNull Object value) { + ArgumentUtils.requireNonNull("key", key); + ArgumentUtils.requireNonNull("value", value); + if (mapCache != null) { + mapCache.fastPut(key, value, configuration.getExpireAfterWrite().toMillis(), TimeUnit.MILLISECONDS, + configuration.getExpireAfterAccess().toMillis(), TimeUnit.MILLISECONDS); + } else { + mapCache.fastPut(key, value); + } + } + + @NonNull + @Override + public AsyncCache> async() { + return new RedissonAsyncCache(mapCache, map, executorService, conversionService); + } +} diff --git a/redisson-micronaut/src/main/java/org/redisson/micronaut/session/AttributeMessage.java b/redisson-micronaut/src/main/java/org/redisson/micronaut/session/AttributeMessage.java new file mode 100644 index 000000000..114378fe7 --- /dev/null +++ b/redisson-micronaut/src/main/java/org/redisson/micronaut/session/AttributeMessage.java @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2013-2021 Nikita Koksharov + * + * Licensed 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.redisson.micronaut.session; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import org.redisson.client.protocol.Decoder; +import org.redisson.client.protocol.Encoder; + +import java.io.IOException; +import java.io.Serializable; + +/** + * + * @author Nikita Koksharov + * + */ +public class AttributeMessage implements Serializable { + + private String sessionId; + + private String nodeId; + + public AttributeMessage() { + } + + public AttributeMessage(String nodeId, String sessionId) { + this.nodeId = nodeId; + this.sessionId = sessionId; + } + + public String getSessionId() { + return sessionId; + } + + public String getNodeId() { + return nodeId; + } + + protected byte[] toByteArray(Encoder encoder, Object value) throws IOException { + if (value == null) { + return null; + } + + ByteBuf buf = encoder.encode(value); + try { + return ByteBufUtil.getBytes(buf); + } finally { + buf.release(); + } + } + + protected Object toObject(Decoder decoder, byte[] value) throws IOException, ClassNotFoundException { + if (value == null) { + return null; + } + + ByteBuf buf = Unpooled.wrappedBuffer(value); + try { + return decoder.decode(buf, null); + } finally { + buf.release(); + } + } +} diff --git a/redisson-micronaut/src/main/java/org/redisson/micronaut/session/AttributeRemoveMessage.java b/redisson-micronaut/src/main/java/org/redisson/micronaut/session/AttributeRemoveMessage.java new file mode 100644 index 000000000..8d43784f1 --- /dev/null +++ b/redisson-micronaut/src/main/java/org/redisson/micronaut/session/AttributeRemoveMessage.java @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2013-2021 Nikita Koksharov + * + * Licensed 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.redisson.micronaut.session; + +import java.util.Set; + +/** + * + * @author Nikita Koksharov + * + */ +public class AttributeRemoveMessage extends AttributeMessage { + + private Set names; + + public AttributeRemoveMessage() { + super(); + } + + public AttributeRemoveMessage(String nodeId, String sessionId, Set names) { + super(nodeId, sessionId); + this.names = names; + } + + public Set getNames() { + return names; + } + +} diff --git a/redisson-micronaut/src/main/java/org/redisson/micronaut/session/AttributeUpdateMessage.java b/redisson-micronaut/src/main/java/org/redisson/micronaut/session/AttributeUpdateMessage.java new file mode 100644 index 000000000..f81987bb2 --- /dev/null +++ b/redisson-micronaut/src/main/java/org/redisson/micronaut/session/AttributeUpdateMessage.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2013-2021 Nikita Koksharov + * + * Licensed 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.redisson.micronaut.session; + +import org.redisson.client.protocol.Decoder; +import org.redisson.client.protocol.Encoder; + +import java.io.IOException; + +/** + * + * @author Nikita Koksharov + * + */ +public class AttributeUpdateMessage extends AttributeMessage { + + private String name; + private byte[] value; + + public AttributeUpdateMessage() { + } + + public AttributeUpdateMessage(String nodeId, String sessionId, String name, Object value, Encoder encoder) throws IOException { + super(nodeId, sessionId); + this.name = name; + this.value = toByteArray(encoder, value); + } + + public String getName() { + return name; + } + + public Object getValue(Decoder decoder) throws IOException, ClassNotFoundException { + return toObject(decoder, value); + } + +} diff --git a/redisson-micronaut/src/main/java/org/redisson/micronaut/session/AttributesClearMessage.java b/redisson-micronaut/src/main/java/org/redisson/micronaut/session/AttributesClearMessage.java new file mode 100644 index 000000000..e6af50017 --- /dev/null +++ b/redisson-micronaut/src/main/java/org/redisson/micronaut/session/AttributesClearMessage.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2013-2021 Nikita Koksharov + * + * Licensed 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.redisson.micronaut.session; + +/** + * + * @author Nikita Koksharov + * + */ +public class AttributesClearMessage extends AttributeMessage { + + public AttributesClearMessage() { + } + + public AttributesClearMessage(String nodeId, String sessionId) { + super(nodeId, sessionId); + } + +} diff --git a/redisson-micronaut/src/main/java/org/redisson/micronaut/session/AttributesPutAllMessage.java b/redisson-micronaut/src/main/java/org/redisson/micronaut/session/AttributesPutAllMessage.java new file mode 100644 index 000000000..d09429c89 --- /dev/null +++ b/redisson-micronaut/src/main/java/org/redisson/micronaut/session/AttributesPutAllMessage.java @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2013-2021 Nikita Koksharov + * + * Licensed 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.redisson.micronaut.session; + +import org.redisson.client.protocol.Decoder; +import org.redisson.client.protocol.Encoder; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +/** + * + * @author Nikita Koksharov + * + */ +public class AttributesPutAllMessage extends AttributeMessage { + + private Map attrs; + + public AttributesPutAllMessage() { + } + + public AttributesPutAllMessage(String nodeId, String sessionId, Map attrs, Encoder encoder) throws IOException { + super(nodeId, sessionId); + if (attrs != null) { + this.attrs = new HashMap<>(); + for (Entry entry: attrs.entrySet()) { + this.attrs.put(entry.getKey(), toByteArray(encoder, entry.getValue())); + } + } else { + this.attrs = null; + } + } + + public Map getAttrs(Decoder decoder) throws IOException, ClassNotFoundException { + if (attrs == null) { + return null; + } + Map result = new HashMap<>(); + for (Entry entry: attrs.entrySet()) { + result.put(entry.getKey(), toObject(decoder, entry.getValue())); + } + return result; + } + +} diff --git a/redisson-micronaut/src/main/java/org/redisson/micronaut/session/RedissonHttpSessionConfiguration.java b/redisson-micronaut/src/main/java/org/redisson/micronaut/session/RedissonHttpSessionConfiguration.java new file mode 100644 index 000000000..62ad96546 --- /dev/null +++ b/redisson-micronaut/src/main/java/org/redisson/micronaut/session/RedissonHttpSessionConfiguration.java @@ -0,0 +1,99 @@ +/** + * Copyright (c) 2013-2021 Nikita Koksharov + * + * Licensed 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.redisson.micronaut.session; + +import io.micronaut.context.annotation.ConfigurationProperties; +import io.micronaut.core.util.Toggleable; +import io.micronaut.session.http.HttpSessionConfiguration; +import org.redisson.client.codec.Codec; + +/** + * Micronaut Session settings. + * + * @author Nikita Koksharov + */ +@ConfigurationProperties("redisson") +public class RedissonHttpSessionConfiguration extends HttpSessionConfiguration implements Toggleable { + + public enum UpdateMode {WRITE_BEHIND, AFTER_REQUEST} + + private String keyPrefix = ""; + private Codec codec; + private UpdateMode updateMode = UpdateMode.AFTER_REQUEST; + private boolean broadcastSessionUpdates = false; + + public boolean isBroadcastSessionUpdates() { + return broadcastSessionUpdates; + } + + /** + * Defines broadcasting of session updates across all micronaut services. + * + * @param broadcastSessionUpdates - if true then session changes are broadcasted. + */ + public void setBroadcastSessionUpdates(boolean broadcastSessionUpdates) { + this.broadcastSessionUpdates = broadcastSessionUpdates; + } + + public UpdateMode getUpdateMode() { + return updateMode; + } + + /** + * Defines session attributes update mode. + *

+ * WRITE_BEHIND - session changes stored asynchronously. + * AFTER_REQUEST - session changes stored only on io.micronaut.session.SessionStore#save(io.micronaut.session.Session) method invocation. + *

+ * Default is AFTER_REQUEST. + * + * @param updateMode - mode value + */ + public void setUpdateMode(UpdateMode updateMode) { + this.updateMode = updateMode; + } + + public Codec getCodec() { + return codec; + } + + /** + * Redis data codec applied to session values. + * Default is MarshallingCodec codec + * + * @see org.redisson.client.codec.Codec + * @see org.redisson.codec.MarshallingCodec + * + * @param codec - data codec + * @return config + */ + public void setCodec(Codec codec) { + this.codec = codec; + } + + public String getKeyPrefix() { + return keyPrefix; + } + + /** + * Defines string prefix applied to all objects stored in Redis. + * + * @param keyPrefix - key prefix value + */ + public void setKeyPrefix(String keyPrefix) { + this.keyPrefix = keyPrefix; + } +} diff --git a/redisson-micronaut/src/main/java/org/redisson/micronaut/session/RedissonSession.java b/redisson-micronaut/src/main/java/org/redisson/micronaut/session/RedissonSession.java new file mode 100644 index 000000000..d64ea38c2 --- /dev/null +++ b/redisson-micronaut/src/main/java/org/redisson/micronaut/session/RedissonSession.java @@ -0,0 +1,283 @@ +/** + * Copyright (c) 2013-2021 Nikita Koksharov + * + * Licensed 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.redisson.micronaut.session; + +import edu.umd.cs.findbugs.annotations.NonNull; +import io.micronaut.core.convert.value.MutableConvertibleValues; +import io.micronaut.session.InMemorySession; +import io.micronaut.session.Session; +import org.redisson.api.*; +import org.redisson.client.codec.IntegerCodec; + +import java.io.IOException; +import java.time.Duration; +import java.time.Instant; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +/** + * + * @author Nikita Koksharov + * + */ +public class RedissonSession extends InMemorySession implements Session { + + private static final String MAX_INACTIVE_INTERVAL_ATTR = "session:maxInactiveInterval"; + private static final String LAST_ACCESSED_TIME_ATTR = "session:lastAccessedTime"; + private static final String CREATION_TIME_ATTR = "session:creationTime"; + + private final RedissonSessionStore redissonManager; + private final RMap map; + private final RTopic topic; + private final RedissonHttpSessionConfiguration.UpdateMode updateMode; + private Instant creationTime; + private boolean broadcastSessionUpdates; + + private Set removedAttributes = Collections.emptySet(); + private Map updatedAttributes = Collections.emptyMap(); + + public RedissonSession(RedissonSessionStore redissonManager, + String id, + RedissonHttpSessionConfiguration.UpdateMode updateMode) { + this(redissonManager, id, updateMode, Duration.ZERO); + } + + public RedissonSession(RedissonSessionStore redissonManager, + String id, + RedissonHttpSessionConfiguration.UpdateMode updateMode, + Duration maxInactiveInterval) { + super(id, maxInactiveInterval); + this.redissonManager = redissonManager; + this.updateMode = updateMode; + this.topic = redissonManager.getTopic(); + + if (updateMode == RedissonHttpSessionConfiguration.UpdateMode.AFTER_REQUEST) { + removedAttributes = Collections.newSetFromMap(new ConcurrentHashMap<>()); + updatedAttributes = new ConcurrentHashMap<>(); + } + + this.creationTime = super.getCreationTime(); + super.setLastAccessedTime(creationTime); + map = redissonManager.getMap(getId()); + } + + @NonNull + @Override + public Instant getCreationTime() { + return creationTime; + } + + @Override + public MutableConvertibleValues clear() { + if (!isNew()) { + removedAttributes.addAll(names()); + if (updateMode == RedissonHttpSessionConfiguration.UpdateMode.WRITE_BEHIND && map != null) { + delete(); + } + } + return super.clear(); + } + + public CompletableFuture delete() { + RBatch batch = redissonManager.createBatch(); + RMapAsync m = batch.getMap(map.getName(), map.getCodec()); + + RBucketAsync b = batch.getBucket(redissonManager.getNotificationBucket(getId()).getName(), IntegerCodec.INSTANCE); + b.deleteAsync(); + m.fastPutAsync(LAST_ACCESSED_TIME_ATTR, 0L); + m.expireAsync(10, TimeUnit.SECONDS); + if (broadcastSessionUpdates) { + RTopicAsync t = batch.getTopic(topic.getChannelNames().get(0)); + t.publishAsync(new AttributesClearMessage(redissonManager.getNodeId(), getId())); + } + return batch.executeAsync().thenApply(s -> (Void)null).toCompletableFuture(); + } + + protected void expireSession() { + if (getMaxInactiveInterval().getSeconds() >= 0) { + RBatch batch = redissonManager.createBatch(); + RMapAsync m = batch.getMap(map.getName(), map.getCodec()); + RBucketAsync b = batch.getBucket(redissonManager.getNotificationBucket(getId()).getName(), IntegerCodec.INSTANCE); + b.setAsync(1); + b.expireAsync(getMaxInactiveInterval().getSeconds(), TimeUnit.SECONDS); + m.expireAsync(getMaxInactiveInterval().getSeconds() + 10, TimeUnit.SECONDS); + batch.executeAsync(); + } + } + + protected AttributesPutAllMessage createPutAllMessage(Map newMap) { + Map map = new HashMap<>(); + for (Map.Entry entry : newMap.entrySet()) { + map.put(entry.getKey(), entry.getValue()); + } + try { + return new AttributesPutAllMessage(redissonManager.getNodeId(), getId(), map, this.map.getCodec().getMapValueEncoder()); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + @Override + public Session setMaxInactiveInterval(Duration duration) { + if (updateMode == RedissonHttpSessionConfiguration.UpdateMode.WRITE_BEHIND && map != null) { + fastPut(MAX_INACTIVE_INTERVAL_ATTR, duration.toMillis()); + expireSession(); + } + + if (updateMode == RedissonHttpSessionConfiguration.UpdateMode.AFTER_REQUEST) { + updatedAttributes.put(MAX_INACTIVE_INTERVAL_ATTR, duration.toMillis()); + } + + return super.setMaxInactiveInterval(duration); + } + + private void fastPut(String name, Object value) { + if (map == null) { + return; + } + map.fastPutAsync(name, value); + try { + if (broadcastSessionUpdates) { + topic.publishAsync(new AttributeUpdateMessage(redissonManager.getNodeId(), getId(), name, value, this.map.getCodec().getMapValueEncoder())); + } + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + @Override + public Session setLastAccessedTime(Instant instant) { + if (updateMode == RedissonHttpSessionConfiguration.UpdateMode.WRITE_BEHIND && map != null) { + fastPut(LAST_ACCESSED_TIME_ATTR, instant.toEpochMilli()); + expireSession(); + } + + if (updateMode == RedissonHttpSessionConfiguration.UpdateMode.AFTER_REQUEST) { + updatedAttributes.put(LAST_ACCESSED_TIME_ATTR, instant.toEpochMilli()); + } + + return super.setLastAccessedTime(instant); + } + + public void superPut(CharSequence name, Object value) { + super.put(name, value); + } + + @Override + public MutableConvertibleValues put(CharSequence key, Object value) { + if (value == null) { + return super.put(key, value); + } + + if (updateMode == RedissonHttpSessionConfiguration.UpdateMode.WRITE_BEHIND && map != null) { + fastPut(key.toString(), value); + } + if (updateMode == RedissonHttpSessionConfiguration.UpdateMode.AFTER_REQUEST) { + updatedAttributes.put(key.toString(), value); + removedAttributes.remove(key.toString()); + } + + return super.put(key, value); + } + + public void superRemove(CharSequence key) { + super.remove(key); + } + + @Override + public MutableConvertibleValues remove(CharSequence key) { + if (updateMode == RedissonHttpSessionConfiguration.UpdateMode.WRITE_BEHIND && map != null) { + map.fastRemoveAsync(key.toString()); + if (broadcastSessionUpdates) { + topic.publishAsync(new AttributeRemoveMessage(redissonManager.getNodeId(), getId(), new HashSet<>(Arrays.asList(key)))); + } + } + if (updateMode == RedissonHttpSessionConfiguration.UpdateMode.AFTER_REQUEST) { + updatedAttributes.remove(key.toString()); + removedAttributes.add(key.toString()); + } + + return super.remove(key); + } + + public CompletableFuture save() { + Map newMap = new HashMap<>(); + if (isNew() || updateMode == RedissonHttpSessionConfiguration.UpdateMode.WRITE_BEHIND) { + newMap.put(LAST_ACCESSED_TIME_ATTR, getLastAccessedTime().toEpochMilli()); + newMap.put(MAX_INACTIVE_INTERVAL_ATTR, getMaxInactiveInterval().toMillis()); + newMap.put(CREATION_TIME_ATTR, getCreationTime().toEpochMilli()); + for (Map.Entry entry : attributeMap.entrySet()) { + newMap.put(entry.getKey(), entry.getValue()); + } + } else { + newMap.putAll(updatedAttributes); + } + + if (newMap.isEmpty()) { + return CompletableFuture.completedFuture(this); + } + + RBatch batch = redissonManager.createBatch(); + RMapAsync m = batch.getMap(map.getName(), map.getCodec()); + m.putAllAsync(newMap); + m.fastRemoveAsync(removedAttributes.toArray(new String[0])); + RBucketAsync bucket = batch.getBucket(redissonManager.getNotificationBucket(getId()).getName(), IntegerCodec.INSTANCE); + bucket.setAsync(1); + + if (broadcastSessionUpdates) { + RTopicAsync t = batch.getTopic(topic.getChannelNames().get(0)); + t.publishAsync(createPutAllMessage(newMap)); + + if (updateMode == RedissonHttpSessionConfiguration.UpdateMode.AFTER_REQUEST) { + if (!removedAttributes.isEmpty()) { + t.publishAsync(new AttributeRemoveMessage(redissonManager.getNodeId(), getId(), new HashSet<>(removedAttributes))); + } + } + } + + removedAttributes.clear(); + updatedAttributes.clear(); + + if (getMaxInactiveInterval().getSeconds() >= 0) { + bucket.expireAsync(getMaxInactiveInterval().getSeconds(), TimeUnit.SECONDS); + m.expireAsync(getMaxInactiveInterval().getSeconds() + 10, TimeUnit.SECONDS); + } + return batch.executeAsync().thenApply(b -> this).toCompletableFuture(); + } + + public void load(Map attrs) { + Long creationTime = (Long) attrs.remove(CREATION_TIME_ATTR); + if (creationTime != null) { + this.creationTime = Instant.ofEpochMilli(creationTime); + } + Long lastAccessedTime = (Long) attrs.remove(LAST_ACCESSED_TIME_ATTR); + if (lastAccessedTime != null) { + super.setLastAccessedTime(Instant.ofEpochMilli(lastAccessedTime)); + } + Long maxInactiveInterval = (Long) attrs.remove(MAX_INACTIVE_INTERVAL_ATTR); + if (maxInactiveInterval != null) { + super.setMaxInactiveInterval(Duration.ofMillis(maxInactiveInterval)); + } + setNew(false); + + for (Map.Entry entry : attrs.entrySet()) { + attributeMap.put(entry.getKey(), entry.getValue()); + } + } + +} diff --git a/redisson-micronaut/src/main/java/org/redisson/micronaut/session/RedissonSessionStore.java b/redisson-micronaut/src/main/java/org/redisson/micronaut/session/RedissonSessionStore.java new file mode 100644 index 000000000..4b516881a --- /dev/null +++ b/redisson-micronaut/src/main/java/org/redisson/micronaut/session/RedissonSessionStore.java @@ -0,0 +1,276 @@ +/** + * Copyright (c) 2013-2021 Nikita Koksharov + * + * Licensed 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.redisson.micronaut.session; + +import io.micronaut.context.BeanLocator; +import io.micronaut.context.annotation.Primary; +import io.micronaut.context.annotation.Replaces; +import io.micronaut.context.annotation.Requires; +import io.micronaut.context.event.ApplicationEventPublisher; +import io.micronaut.core.serialize.ObjectSerializer; +import io.micronaut.core.util.StringUtils; +import io.micronaut.scheduling.TaskExecutors; +import io.micronaut.session.InMemorySessionStore; +import io.micronaut.session.SessionIdGenerator; +import io.micronaut.session.SessionSettings; +import io.micronaut.session.SessionStore; +import io.micronaut.session.event.SessionCreatedEvent; +import io.micronaut.session.event.SessionDeletedEvent; +import io.micronaut.session.event.SessionExpiredEvent; +import org.redisson.api.*; +import org.redisson.api.listener.MessageListener; +import org.redisson.api.listener.PatternMessageListener; +import org.redisson.client.codec.Codec; +import org.redisson.client.codec.IntegerCodec; +import org.redisson.client.codec.StringCodec; +import org.redisson.codec.CompositeCodec; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Named; +import javax.inject.Singleton; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; + +/** + * + * @author Nikita Koksharov + * + */ +@Singleton +@Primary +@Requires(property = RedissonSessionStore.ENABLED, value = StringUtils.TRUE) +@Replaces(InMemorySessionStore.class) +public class RedissonSessionStore implements SessionStore, PatternMessageListener, MessageListener { + + public static final String ENABLED = SessionSettings.HTTP + ".redisson.enabled"; + + private static final String SESSION_PREFIX = "redisson:session:"; + + private MessageListener messageListener; + + private final String nodeId = UUID.randomUUID().toString(); + + private static final Logger LOG = LoggerFactory.getLogger(RedissonSessionStore.class); + + private RPatternTopic deletedTopic; + private RPatternTopic expiredTopic; + private RTopic createdTopic; + private RedissonClient redisson; + + private final SessionIdGenerator sessionIdGenerator; + private RedissonHttpSessionConfiguration sessionConfiguration; + private final ApplicationEventPublisher eventPublisher; + + public RedissonSessionStore( + RedissonClient redisson, + SessionIdGenerator sessionIdGenerator, + RedissonHttpSessionConfiguration sessionConfiguration, + ApplicationEventPublisher eventPublisher) { + + this.sessionIdGenerator = sessionIdGenerator; + this.sessionConfiguration = sessionConfiguration; + this.eventPublisher = eventPublisher; + this.redisson = redisson; + + deletedTopic = redisson.getPatternTopic("__keyevent@*:del", StringCodec.INSTANCE); + expiredTopic = redisson.getPatternTopic("__keyevent@*:expired", StringCodec.INSTANCE); + createdTopic = redisson.getTopic(getEventsChannelPrefix(), StringCodec.INSTANCE); + + deletedTopic.addListener(String.class, this); + expiredTopic.addListener(String.class, this); + createdTopic.addListener(String.class, this); + + if (sessionConfiguration.isBroadcastSessionUpdates()) { + RTopic updatesTopic = getTopic(); + messageListener = new MessageListener() { + + @Override + public void onMessage(CharSequence channel, AttributeMessage msg) { + if (msg.getNodeId().equals(nodeId)) { + return; + } + + findSession(msg.getSessionId()).thenAccept(s -> { + if (s.isPresent()) { + return; + } + + try { + RedissonSession session = s.get(); + if (msg instanceof AttributeRemoveMessage) { + for (CharSequence name : ((AttributeRemoveMessage)msg).getNames()) { + session.superRemove(name); + } + } + + if (msg instanceof AttributesClearMessage) { + deleteSession(session.getId()); + } + + if (msg instanceof AttributesPutAllMessage) { + AttributesPutAllMessage m = (AttributesPutAllMessage) msg; + Map attrs = m.getAttrs(getCodec().getMapValueDecoder()); + session.load(attrs); + } + + if (msg instanceof AttributeUpdateMessage) { + AttributeUpdateMessage m = (AttributeUpdateMessage)msg; + session.superPut(m.getName(), m.getValue(getCodec().getMapValueDecoder())); + } + } catch (Exception e) { + LOG.error("Unable to handle topic message", e); + } + }); + } + }; + + updatesTopic.addListener(AttributeMessage.class, messageListener); + } + + } + + String getEventsChannelPrefix() { + return sessionConfiguration.getKeyPrefix() + "sessions:created:"; + } + + String getExpiredKeyPrefix() { + return sessionConfiguration.getKeyPrefix() + "sessions:expires:"; + } + + @Override + public RedissonSession newSession() { + return new RedissonSession(this, sessionIdGenerator.generateId(), + sessionConfiguration.getUpdateMode(), sessionConfiguration.getMaxInactiveInterval()); + } + + @Override + public CompletableFuture> findSession(String id) { + return loadSession(id, false); + } + + @Override + public CompletableFuture deleteSession(String id) { + return loadSession(id, false).thenCompose(optional -> { + return optional.map(s -> { + return s.delete().thenApply(r -> { + return true; + }); + }).orElse(CompletableFuture.completedFuture(false)); + }).toCompletableFuture(); + } + + @Override + public CompletableFuture save(RedissonSession session) { + CompletableFuture f = session.save(); + return f.thenCompose(v -> { + if (session.isNew()) { + return createdTopic.publishAsync(v.getId()).thenApply(val -> v); + } + return CompletableFuture.completedFuture(session); + }); + } + + @Override + public void onMessage(CharSequence pattern, CharSequence channel, String body) { + if (deletedTopic.getPatternNames().contains(pattern.toString())) { + if (!body.contains(SESSION_PREFIX +"notification:")) { + return; + } + + String id = body.split(SESSION_PREFIX +"notification:")[1]; + loadSession(id, true).whenComplete((r, e) -> { + r.ifPresent(v -> { + eventPublisher.publishEvent(new SessionDeletedEvent(v)); + }); + }); + } else if (expiredTopic.getPatternNames().contains(pattern.toString())) { + if (!body.contains(SESSION_PREFIX +"notification:")) { + return; + } + + String id = body.split(SESSION_PREFIX +"notification:")[1]; + loadSession(id, true).whenComplete((r, e) -> { + r.ifPresent(v -> { + eventPublisher.publishEvent(new SessionExpiredEvent(v)); + }); + }); + } + } + + private CompletableFuture> loadSession(String id, boolean useExpired) { + RMap map = getMap(id); + + return map.readAllMapAsync().thenApply(data -> { + if (data.isEmpty()) { + return Optional.empty(); + } + RedissonSession session = new RedissonSession(this, id, + sessionConfiguration.getUpdateMode()); + session.load(data); + if (useExpired || !session.isExpired()) { + return Optional.of(session); + } + return Optional.empty(); + }).toCompletableFuture(); + } + + @Override + public void onMessage(CharSequence channel, String id) { + loadSession(id, true).whenComplete((r, e) -> { + r.ifPresent(v -> { + eventPublisher.publishEvent(new SessionCreatedEvent(v)); + }); + }); + } + + public RTopic getTopic() { + String keyPrefix = sessionConfiguration.getKeyPrefix(); + String separator = keyPrefix == null || keyPrefix.isEmpty() ? "" : ":"; + final String name = keyPrefix + separator + "redisson:session_updates"; + return redisson.getTopic(name); + } + + public String getNodeId() { + return nodeId; + } + + public RBatch createBatch() { + return redisson.createBatch(); + } + + private Codec getCodec() { + return Optional.ofNullable(sessionConfiguration.getCodec()).orElse(redisson.getConfig().getCodec()); + } + + public RMap getMap(String sessionId) { + String keyPrefix = sessionConfiguration.getKeyPrefix(); + String separator = keyPrefix == null || keyPrefix.isEmpty() ? "" : ":"; + String name = keyPrefix + separator + SESSION_PREFIX + sessionId; + return redisson.getMap(name, new CompositeCodec(StringCodec.INSTANCE, getCodec(), getCodec())); + } + + public RBucket getNotificationBucket(String sessionId) { + String keyPrefix = sessionConfiguration.getKeyPrefix(); + String separator = keyPrefix == null || keyPrefix.isEmpty() ? "" : ":"; + String name = keyPrefix + separator + SESSION_PREFIX +"notification:" + sessionId; + return redisson.getBucket(name, IntegerCodec.INSTANCE); + } + +} diff --git a/redisson-micronaut/src/main/java/org/redisson/micronaut/session/SessionCreatedMessage.java b/redisson-micronaut/src/main/java/org/redisson/micronaut/session/SessionCreatedMessage.java new file mode 100644 index 000000000..a5ec1b1e0 --- /dev/null +++ b/redisson-micronaut/src/main/java/org/redisson/micronaut/session/SessionCreatedMessage.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2013-2021 Nikita Koksharov + * + * Licensed 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.redisson.micronaut.session; + +/** + * + * @author Nikita Koksharov + * + */ +public class SessionCreatedMessage extends AttributeMessage { + + public SessionCreatedMessage() { + } + + public SessionCreatedMessage(String nodeId, String sessionId) { + super(nodeId, sessionId); + } + +} diff --git a/redisson-micronaut/src/main/java/org/redisson/micronaut/session/SessionDestroyedMessage.java b/redisson-micronaut/src/main/java/org/redisson/micronaut/session/SessionDestroyedMessage.java new file mode 100644 index 000000000..33f2c9c0d --- /dev/null +++ b/redisson-micronaut/src/main/java/org/redisson/micronaut/session/SessionDestroyedMessage.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2013-2021 Nikita Koksharov + * + * Licensed 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.redisson.micronaut.session; + +/** + * + * @author Nikita Koksharov + * + */ +public class SessionDestroyedMessage extends AttributeMessage { + + public SessionDestroyedMessage() { + } + + public SessionDestroyedMessage(String nodeId, String sessionId) { + super(nodeId, sessionId); + } + +} diff --git a/redisson-micronaut/src/main/resources/META-INF/native-image/org.redisson/redisson/native-image.properties b/redisson-micronaut/src/main/resources/META-INF/native-image/org.redisson/redisson/native-image.properties new file mode 100644 index 000000000..e58d6dd87 --- /dev/null +++ b/redisson-micronaut/src/main/resources/META-INF/native-image/org.redisson/redisson/native-image.properties @@ -0,0 +1 @@ +Args=--initialize-at-build-time=org.redisson.RedissonReference,org.redisson.executor.CronExpression,org.redisson.liveobject.core.RedissonObjectBuilder diff --git a/redisson-micronaut/src/main/resources/META-INF/native-image/org.redisson/redisson/proxy-config.json b/redisson-micronaut/src/main/resources/META-INF/native-image/org.redisson/redisson/proxy-config.json new file mode 100644 index 000000000..c8bfc53cf --- /dev/null +++ b/redisson-micronaut/src/main/resources/META-INF/native-image/org.redisson/redisson/proxy-config.json @@ -0,0 +1,91 @@ +[ + ["org.redisson.executor.RemoteExecutorServiceAsync"], + ["org.redisson.api.RAtomicDoubleReactive"], + ["org.redisson.api.RAtomicLongReactive"], + ["org.redisson.api.RBatchReactive"], + ["org.redisson.api.RBinaryStreamReactive"], + ["org.redisson.api.RBitSetReactive"], + ["org.redisson.api.RBlockingDequeReactive"], + ["org.redisson.api.RBlockingQueueReactive"], + ["org.redisson.api.RBucketReactive"], + ["org.redisson.api.RBucketsReactive"], + ["org.redisson.api.RCollectionReactive"], + ["org.redisson.api.RCountDownLatchReactive"], + ["org.redisson.api.RDequeReactive"], + ["org.redisson.api.RExpirableReactive"], + ["org.redisson.api.RGeoReactive"], + ["org.redisson.api.RHyperLogLogReactive"], + ["org.redisson.api.RIdGeneratorReactive"], + ["org.redisson.api.RKeysReactive"], + ["org.redisson.api.RLexSortedSetReactive"], + ["org.redisson.api.RListMultimapReactive"], + ["org.redisson.api.RListReactive"], + ["org.redisson.api.RLockReactive"], + ["org.redisson.api.RMapCacheReactive"], + ["org.redisson.api.RMapReactive"], + ["org.redisson.api.RMultimapReactive"], + ["org.redisson.api.RObjectReactive"], + ["org.redisson.api.RPatternTopicReactive"], + ["org.redisson.api.RPermitExpirableSemaphoreReactive"], + ["org.redisson.api.RQueueReactive"], + ["org.redisson.api.RRateLimiterReactive"], + ["org.redisson.api.RReadWriteLockReactive"], + ["org.redisson.api.RReliableTopicReactive"], + ["org.redisson.api.RRingBufferReactive"], + ["org.redisson.api.RScoredSortedSetReactive"], + ["org.redisson.api.RScriptReactive"], + ["org.redisson.api.RSemaphoreReactive"], + ["org.redisson.api.RSetCacheReactive"], + ["org.redisson.api.RSetMultimapReactive"], + ["org.redisson.api.RSetReactive"], + ["org.redisson.api.RSortableReactive"], + ["org.redisson.api.RStreamReactive"], + ["org.redisson.api.RTimeSeriesReactive"], + ["org.redisson.api.RTopicReactive"], + ["org.redisson.api.RTransactionReactive"], + ["org.redisson.api.RTransferQueueReactive"], + ["org.redisson.api.RAtomicDoubleRx"], + ["org.redisson.api.RAtomicLongRx"], + ["org.redisson.api.RBatchRx"], + ["org.redisson.api.RBinaryStreamRx"], + ["org.redisson.api.RBitSetRx"], + ["org.redisson.api.RBlockingDequeRx"], + ["org.redisson.api.RBlockingQueueRx"], + ["org.redisson.api.RBucketRx"], + ["org.redisson.api.RBucketsRx"], + ["org.redisson.api.RCollectionRx"], + ["org.redisson.api.RCountDownLatchRx"], + ["org.redisson.api.RDequeRx"], + ["org.redisson.api.RExpirableRx"], + ["org.redisson.api.RGeoRx"], + ["org.redisson.api.RHyperLogLogRx"], + ["org.redisson.api.RIdGeneratorRx"], + ["org.redisson.api.RKeysRx"], + ["org.redisson.api.RLexSortedSetRx"], + ["org.redisson.api.RListMultimapRx"], + ["org.redisson.api.RListRx"], + ["org.redisson.api.RLockRx"], + ["org.redisson.api.RMapCacheRx"], + ["org.redisson.api.RMapRx"], + ["org.redisson.api.RMultimapRx"], + ["org.redisson.api.RObjectRx"], + ["org.redisson.api.RPatternTopicRx"], + ["org.redisson.api.RPermitExpirableSemaphoreRx"], + ["org.redisson.api.RQueueRx"], + ["org.redisson.api.RRateLimiterRx"], + ["org.redisson.api.RReadWriteLockRx"], + ["org.redisson.api.RReliableTopicRx"], + ["org.redisson.api.RRingBufferRx"], + ["org.redisson.api.RScoredSortedSetRx"], + ["org.redisson.api.RScriptRx"], + ["org.redisson.api.RSemaphoreRx"], + ["org.redisson.api.RSetCacheRx"], + ["org.redisson.api.RSetMultimapRx"], + ["org.redisson.api.RSetRx"], + ["org.redisson.api.RSortableRx"], + ["org.redisson.api.RStreamRx"], + ["org.redisson.api.RTimeSeriesRx"], + ["org.redisson.api.RTopicRx"], + ["org.redisson.api.RTransactionRx"], + ["org.redisson.api.RTransferQueueRx"] +] diff --git a/redisson-micronaut/src/main/resources/META-INF/native-image/org.redisson/redisson/reflect-config.json b/redisson-micronaut/src/main/resources/META-INF/native-image/org.redisson/redisson/reflect-config.json new file mode 100644 index 000000000..e5988bf27 --- /dev/null +++ b/redisson-micronaut/src/main/resources/META-INF/native-image/org.redisson/redisson/reflect-config.json @@ -0,0 +1,479 @@ +[ +{ + "name":"io.netty.buffer.AbstractByteBufAllocator", + "allDeclaredMethods":true +}, +{ + "name":"io.netty.buffer.AbstractReferenceCountedByteBuf", + "fields":[{"name":"refCnt", "allowUnsafeAccess":true}] +}, +{ + "name":"io.netty.util.ReferenceCountUtil", + "allDeclaredMethods":true +}, +{ + "name":"io.netty.channel.EventLoopGroup", + "allDeclaredMethods":true +}, +{ + "name":"io.netty.channel.socket.nio.NioDatagramChannel", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"io.netty.channel.socket.nio.NioSocketChannel", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"io.netty.handler.codec.ByteToMessageDecoder", + "methods":[ + {"name":"channelInactive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }, + {"name":"channelReadComplete","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"userEventTriggered","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] } + ] +}, +{ + "name":"io.netty.handler.codec.MessageToMessageDecoder", + "methods":[{"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }] +}, +{ + "name":"io.netty.handler.codec.MessageToMessageEncoder", + "methods":[{"name":"write","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object","io.netty.channel.ChannelPromise"] }] +}, +{ + "name":"io.netty.handler.codec.dns.DatagramDnsQueryEncoder" +}, +{ + "name":"io.netty.resolver.dns.DnsNameResolver$1" +}, +{ + "name":"io.netty.resolver.dns.DnsNameResolver$3" +}, +{ + "name":"io.netty.resolver.dns.DnsNameResolver$DnsResponseHandler", + "methods":[ + {"name":"channelActive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }, + {"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] } + ] +}, +{ + "name":"io.netty.util.internal.shaded.org.jctools.queues.BaseMpscLinkedArrayQueueColdProducerFields", + "fields":[{"name":"producerLimit", "allowUnsafeAccess":true}] +}, +{ + "name":"io.netty.util.internal.shaded.org.jctools.queues.BaseMpscLinkedArrayQueueConsumerFields", + "fields":[{"name":"consumerIndex", "allowUnsafeAccess":true}] +}, +{ + "name":"io.netty.util.internal.shaded.org.jctools.queues.BaseMpscLinkedArrayQueueProducerFields", + "fields":[{"name":"producerIndex", "allowUnsafeAccess":true}] +}, +{ + "name":"io.netty.util.internal.shaded.org.jctools.queues.MpscArrayQueueConsumerIndexField", + "fields":[{"name":"consumerIndex", "allowUnsafeAccess":true}] +}, +{ + "name":"io.netty.util.internal.shaded.org.jctools.queues.MpscArrayQueueProducerIndexField", + "fields":[{"name":"producerIndex", "allowUnsafeAccess":true}] +}, +{ + "name":"io.netty.util.internal.shaded.org.jctools.queues.MpscArrayQueueProducerLimitField", + "fields":[{"name":"producerLimit", "allowUnsafeAccess":true}] +}, +{ + "name":"java.sql.Date" +}, +{ + "name":"java.sql.Timestamp" +}, +{ + "name":"java.util.Collections$ReverseComparator2", + "allDeclaredFields":true +}, +{ + "name":"java.util.Collections$UnmodifiableCollection", + "allDeclaredFields":true +}, +{ + "name":"java.util.Collections$UnmodifiableList", + "allDeclaredFields":true +}, +{ + "name":"java.util.Collections$UnmodifiableMap", + "allDeclaredFields":true +}, +{ + "name":"java.util.Collections$UnmodifiableMap$UnmodifiableEntrySet", + "allDeclaredFields":true, + "methods":[{"name":"","parameterTypes":["java.util.Set"] }] +}, +{ + "name":"java.util.Collections$UnmodifiableRandomAccessList", + "allDeclaredFields":true +}, +{ + "name":"java.util.Collections$UnmodifiableSet", + "allDeclaredFields":true +}, +{ + "name":"java.util.Collections$UnmodifiableSortedMap", + "allDeclaredFields":true +}, +{ + "name":"java.util.Collections$UnmodifiableSortedSet", + "allDeclaredFields":true +}, +{ + "name":"java.util.EnumMap", + "fields":[{"name":"keyType"}] +}, +{ + "name":"java.util.EnumSet$SerializationProxy", + "fields":[ + {"name":"elementType"}, + {"name":"elements"} + ] +}, +{ + "name":"java.util.HashSet", + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, +{ + "name":"org.redisson.RedissonAtomicDouble", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonAtomicLong", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonBatch", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonBinaryStream", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonBitSet", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonBlockingDeque", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonBlockingQueue", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonBloomFilter", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonBoundedBlockingQueue", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonBucket", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonBuckets", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonCountDownLatch", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonDelayedQueue", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonDeque", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonDoubleAdder", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonExecutorService", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonFairLock", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonGeo", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonHyperLogLog", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonIdGenerator", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonKeys", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonLexSortedSet", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonList", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonListMultimap", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonListMultimapCache", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonListMultimapValues", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonLiveObjectService", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonLocalCachedMap", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonLock", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonLongAdder", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonMap", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonMapCache", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonMultiLock", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonMultimap", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonMultimapCache", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonPatternTopic", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonPermitExpirableSemaphore", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonPriorityBlockingDeque", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonPriorityBlockingQueue", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonPriorityDeque", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonPriorityQueue", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonQueue", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonQueueSemaphore", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonRateLimiter", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonReadLock", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonReadWriteLock", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonRedLock", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonReliableTopic", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonRemoteService", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonRingBuffer", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonScoredSortedSet", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonScript", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonSemaphore", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonSet", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonSetCache", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonSetMultimap", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonSetMultimapCache", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonSetMultimapValues", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonSortedSet", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonSpinLock", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonStream", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonTimeSeries", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonTopic", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonTransferQueue", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.RedissonWriteLock", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.config.BaseConfig", + "allDeclaredFields":true, + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.config.BaseMasterSlaveServersConfig", + "allDeclaredFields":true, + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.config.ClusterServersConfig", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, +{ + "name":"org.redisson.config.Config", + "allDeclaredFields":true, + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.config.ConfigSupport$ClassMixIn", + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.config.ConfigSupport$ConfigMixIn", + "allDeclaredFields":true, + "allDeclaredMethods":true +}, +{ + "name":"org.redisson.config.MasterSlaveServersConfig", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, +{ + "name":"org.redisson.config.RedissonNodeFileConfig", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, +{ + "name":"org.redisson.config.ReplicatedServersConfig", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, +{ + "name":"org.redisson.config.SentinelServersConfig", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, +{ + "name":"org.redisson.config.SingleServerConfig", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, +{ + "name":"org.redisson.executor.RemoteExecutorService", + "methods":[ + {"name":"executeCallable","parameterTypes":["org.redisson.executor.params.TaskParameters"] }, + {"name":"executeRunnable","parameterTypes":["org.redisson.executor.params.TaskParameters"] }, + {"name":"schedule","parameterTypes":["org.redisson.executor.params.ScheduledCronExpressionParameters"] }, + {"name":"scheduleAtFixedRate","parameterTypes":["org.redisson.executor.params.ScheduledAtFixedRateParameters"] }, + {"name":"scheduleCallable","parameterTypes":["org.redisson.executor.params.ScheduledParameters"] }, + {"name":"scheduleRunnable","parameterTypes":["org.redisson.executor.params.ScheduledParameters"] }, + {"name":"scheduleWithFixedDelay","parameterTypes":["org.redisson.executor.params.ScheduledWithFixedDelayParameters"] } + ] +}, +{ + "name":"org.redisson.executor.RemoteExecutorServiceAsync", + "allPublicMethods":true +} +] diff --git a/redisson-micronaut/src/main/resources/META-INF/native-image/org.redisson/redisson/resource-config.json b/redisson-micronaut/src/main/resources/META-INF/native-image/org.redisson/redisson/resource-config.json new file mode 100644 index 000000000..8c00d0c5e --- /dev/null +++ b/redisson-micronaut/src/main/resources/META-INF/native-image/org.redisson/redisson/resource-config.json @@ -0,0 +1,8 @@ +{ + "resources":{ + "includes":[ + {"pattern":"\\QMETA-INF/MANIFEST.MF\\E"}, + {"pattern":"\\QMETA-INF/services/org.jboss.marshalling.ProviderDescriptor\\E"} + ]}, + "bundles":[] +} diff --git a/redisson-micronaut/src/test/java/org/redisson/micronaut/RedissonCacheTest.java b/redisson-micronaut/src/test/java/org/redisson/micronaut/RedissonCacheTest.java new file mode 100644 index 000000000..d0230d3f7 --- /dev/null +++ b/redisson-micronaut/src/test/java/org/redisson/micronaut/RedissonCacheTest.java @@ -0,0 +1,45 @@ +package org.redisson.micronaut; + +import io.micronaut.context.ApplicationContext; +import io.micronaut.inject.qualifiers.Qualifiers; +import org.junit.jupiter.api.Test; +import org.redisson.api.RedissonClient; +import org.redisson.micronaut.cache.RedissonSyncCache; + +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * + * @author Nikita Koksharov + * + */ +public class RedissonCacheTest { + + @Test + public void test1() throws InterruptedException { + Map map = new HashMap<>(); + map.put("redisson.threads", "10"); + map.put("redisson.single-server-config.address", "redis://127.0.0.1:6379"); +// map.put("redisson.clusterServersConfig.scanInterval", "3333"); +// map.put("redisson.clusterServersConfig.nodeAddresses", Arrays.asList("redis://127.0.0.2:6379","redis://127.0.0.3:6379")); + map.put("redisson.caches.test.expire-after-write", "10s"); + map.put("redisson.caches.test.expire-after-access", "3s"); + ApplicationContext ac = ApplicationContext.run(map); + + RedissonClient client = ac.getBean(RedissonClient.class); + assertThat(client).isNotNull(); + RedissonSyncCache cache = ac.getBean(RedissonSyncCache.class, Qualifiers.byName("test")); + cache.put(1, 2); + Thread.sleep(3500); + assertThat(cache.get(1, Integer.class).isPresent()).isFalse(); + cache.put(3, 4); + Thread.sleep(2000); + cache.get(3, Integer.class); + Thread.sleep(2000); + assertThat(cache.get(3, Integer.class).isPresent()).isTrue(); + } + +} diff --git a/redisson-micronaut/src/test/java/org/redisson/micronaut/RedissonSessionTest.java b/redisson-micronaut/src/test/java/org/redisson/micronaut/RedissonSessionTest.java new file mode 100644 index 000000000..2034a45d5 --- /dev/null +++ b/redisson-micronaut/src/test/java/org/redisson/micronaut/RedissonSessionTest.java @@ -0,0 +1,202 @@ +package org.redisson.micronaut; + +import io.micronaut.context.ApplicationContext; +import io.micronaut.context.event.ApplicationEventListener; +import io.micronaut.session.Session; +import io.micronaut.session.event.AbstractSessionEvent; +import io.micronaut.session.event.SessionCreatedEvent; +import io.micronaut.session.event.SessionDeletedEvent; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.redisson.api.RedissonClient; +import org.redisson.micronaut.session.RedissonSession; +import org.redisson.micronaut.session.RedissonSessionStore; + +import javax.inject.Singleton; +import java.io.Serializable; +import java.time.Duration; +import java.time.Instant; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import static org.assertj.core.api.Assertions.*; + +/** + * @author Nikita Koksharov + */ +public class RedissonSessionTest { + + public static class MyObject implements Serializable { + + private String name; + + public MyObject() { + } + + public MyObject(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + @Singleton + public static class AppListener implements ApplicationEventListener { + + List events = new ArrayList<>(); + + @Override + public void onApplicationEvent(AbstractSessionEvent event) { + events.add(event); + } + + public List getEvents() { + return events; + } + } + + @Test + public void testWriteBehind() throws ExecutionException, InterruptedException { + Map map = new HashMap<>(); + map.put("redisson.threads", "10"); + map.put("micronaut.session.http.redisson.enabled", "true"); + map.put("micronaut.session.http.redisson.updateMode", "WRITE_BEHIND"); + + map.put("redisson.singleServerConfig.address", "redis://127.0.0.1:6379"); + ApplicationContext ac = ApplicationContext.run(map); + RedissonClient rc = ac.getBean(RedissonClient.class); + + rc.getKeys().flushall(); + + RedissonSessionStore sessionStore = ac.getBean(RedissonSessionStore.class); + RedissonSession session = sessionStore.newSession(); + session.put("key1", "oleg"); + session.put("key2", new MyObject("myname")); + session.setMaxInactiveInterval(Duration.ofSeconds(30)); + + RedissonSession saved = sessionStore.save(session).get(); + + saved.remove("key2"); + saved.put("key1", "alba"); + + RedissonSession s = sessionStore.findSession(saved.getId()).get().get(); + assertThat(s.get("key1").get()).isEqualTo("alba"); + assertThat(s.contains("key2")).isFalse(); + + ac.stop(); + } + + @Test + public void testSessionExpiration() throws ExecutionException, InterruptedException { + Map map = new HashMap<>(); + map.put("redisson.threads", "10"); + map.put("micronaut.session.http.redisson.enabled", "true"); + map.put("redisson.singleServerConfig.address", "redis://127.0.0.1:6379"); + ApplicationContext ac = ApplicationContext.run(map); + RedissonClient rc = ac.getBean(RedissonClient.class); + + rc.getKeys().flushall(); + + RedissonSessionStore sessionStore = ac.getBean(RedissonSessionStore.class); + RedissonSession session = sessionStore.newSession(); + session.put("username", "oleg"); + session.put("foo", new MyObject("myname")); + session.setMaxInactiveInterval(Duration.ofSeconds(30)); + + RedissonSession saved = sessionStore.save(session).get(); + testData(saved); + + Thread.sleep(30500); + + Optional noSession = sessionStore.findSession(saved.getId()).get(); + assertThat(noSession).isEmpty(); + + Thread.sleep(10000); + + assertThat(rc.getKeys().count()).isZero(); + + ac.stop(); + } + + @Test + public void testSessionCreate() throws ExecutionException, InterruptedException { + Map map = new HashMap<>(); + map.put("redisson.threads", "10"); + map.put("micronaut.session.http.redisson.enabled", "true"); + map.put("redisson.singleServerConfig.address", "redis://127.0.0.1:6379"); + ApplicationContext ac = ApplicationContext.run(map); + RedissonClient rc = ac.getBean(RedissonClient.class); + AppListener listener = ac.getBean(AppListener.class); + + rc.getKeys().flushall(); + + RedissonSessionStore sessionStore = ac.getBean(RedissonSessionStore.class); + RedissonSession session = sessionStore.newSession(); + session.put("username", "oleg"); + session.put("foo", new MyObject("myname")); + + RedissonSession saved = sessionStore.save(session).get(); + testData(saved); + + assertThat(listener.getEvents()).hasSize(1); + assertThat(listener.getEvents().get(0)).isInstanceOf(SessionCreatedEvent.class); + + listener.getEvents().clear(); + RedissonSession loaded = sessionStore.findSession(saved.getId()).get().get(); + testData(loaded); + + loaded.put("key", "value"); + loaded.remove("username"); + loaded.setLastAccessedTime(Instant.now()); + loaded.setMaxInactiveInterval(Duration.ofMinutes(1)); + sessionStore.save(loaded).get(); + + assertThat(listener.getEvents()).isEmpty(); + + loaded = sessionStore.findSession(saved.getId()).get().get(); + + assertThat(listener.getEvents()).isEmpty(); + assertThat(loaded.contains("username")).isFalse(); + assertThat(((MyObject) loaded.get("foo").get()).getName()).isEqualTo("myname"); + assertThat(loaded.get("key").get()).isEqualTo("value"); + assertThat(loaded.isExpired()).isFalse(); + assertThat(loaded.getCreationTime().getEpochSecond()).isEqualTo(saved.getCreationTime().getEpochSecond()); + assertThat(loaded.getMaxInactiveInterval()).isEqualTo(Duration.ofMinutes(1)); + assertThat(loaded.getId()).isEqualTo(saved.getId()); + + Boolean deleted = sessionStore.deleteSession(saved.getId()).get(); + assertThat(deleted).isTrue(); + + Thread.sleep(1500); + + assertThat(listener.getEvents()).hasSize(1); + assertThat(listener.getEvents().get(0)).isInstanceOf(SessionDeletedEvent.class); + + Optional noSession = sessionStore.findSession(saved.getId()).get(); + assertThat(noSession).isEmpty(); + + Thread.sleep(10000); + + assertThat(rc.getKeys().count()).isZero(); + + ac.stop(); + } + + private void testData(RedissonSession saved) { + assertThat(saved.get("username").get()).isEqualTo("oleg"); + assertThat(((MyObject) saved.get("foo").get()).getName()).isEqualTo("myname"); + assertThat(saved.isExpired()).isFalse(); + assertThat(saved.getCreationTime()).isNotNull(); + assertThat(saved.getMaxInactiveInterval()).isNotNull(); + assertThat(saved.getId()).isNotNull(); + } + + +}