From 736338d48fe26daa4ce43a436fc108165490d59e Mon Sep 17 00:00:00 2001 From: lyrric Date: Wed, 18 Sep 2024 13:37:11 +0800 Subject: [PATCH 1/4] Feature - add computeIfAbsent with TTL method to RMapCache #6136 Signed-off-by: lyrric --- .../java/org/redisson/RedissonMapCache.java | 29 +++++++++++++++++++ .../main/java/org/redisson/api/RMapCache.java | 16 +++++++++- .../org/redisson/api/RMapCacheReactive.java | 16 +++++++++- .../java/org/redisson/api/RMapCacheRx.java | 16 +++++++++- .../org/redisson/RedissonMapCacheTest.java | 16 ++++++++++ 5 files changed, 90 insertions(+), 3 deletions(-) diff --git a/redisson/src/main/java/org/redisson/RedissonMapCache.java b/redisson/src/main/java/org/redisson/RedissonMapCache.java index 30c294197..4b7b62128 100644 --- a/redisson/src/main/java/org/redisson/RedissonMapCache.java +++ b/redisson/src/main/java/org/redisson/RedissonMapCache.java @@ -128,6 +128,35 @@ public class RedissonMapCache extends RedissonMap implements RMapCac Collections.singletonList(optionsName), maxSize, mode); } + @Override + public V computeIfAbsent(K key, Duration ttl, Function mappingFunction) { + checkNotBatch(); + + checkKey(key); + Objects.requireNonNull(mappingFunction); + + V value = get(key); + if (value != null) { + return value; + } + RLock lock = getLock(key); + lock.lock(); + try { + value = get(key); + if (value == null) { + V newValue = mappingFunction.apply(key); + if (newValue != null) { + fastPut(key, newValue, ttl.toMillis(), TimeUnit.MILLISECONDS); + return newValue; + } + return null; + } + return value; + } finally { + lock.unlock(); + } + } + @Override public void setMaxSize(int maxSize) { get(setMaxSizeAsync(maxSize)); diff --git a/redisson/src/main/java/org/redisson/api/RMapCache.java b/redisson/src/main/java/org/redisson/api/RMapCache.java index 559980090..f3c31d0bc 100644 --- a/redisson/src/main/java/org/redisson/api/RMapCache.java +++ b/redisson/src/main/java/org/redisson/api/RMapCache.java @@ -23,6 +23,7 @@ import java.time.Duration; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; +import java.util.function.Function; /** *

Map-based cache with ability to set TTL for each entry via @@ -81,7 +82,20 @@ public interface RMapCache extends RMap, RMapCacheAsync { * @return true if max size has been successfully set, otherwise false. */ boolean trySetMaxSize(int maxSize, EvictionMode mode); - + /** + * If the specified key is not already associated + * with a value, attempts to compute its value using the given mapping function and enters it into this map . + *

+ * Stores value mapped by key with specified time to live. + * Entry expires after specified time to live. + * + * @param key - map key + * @param ttl - time to live for key\value entry. + * If 0 then stores infinitely. + * @param mappingFunction the mapping function to compute a value + * @return current associated value + */ + V computeIfAbsent(K key, Duration ttl, Function mappingFunction); /** * If the specified key is not already associated * with a value, associate it with the given value. diff --git a/redisson/src/main/java/org/redisson/api/RMapCacheReactive.java b/redisson/src/main/java/org/redisson/api/RMapCacheReactive.java index 3ffafdd0f..937637b16 100644 --- a/redisson/src/main/java/org/redisson/api/RMapCacheReactive.java +++ b/redisson/src/main/java/org/redisson/api/RMapCacheReactive.java @@ -24,6 +24,7 @@ import java.time.Duration; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; +import java.util.function.Function; /** *

Map-based cache with ability to set TTL for each entry via @@ -126,7 +127,20 @@ public interface RMapCacheReactive extends RMapReactive, RDestroyabl * @return previous associated value */ Mono putIfAbsent(K key, V value, long ttl, TimeUnit ttlUnit, long maxIdleTime, TimeUnit maxIdleUnit); - + /** + * If the specified key is not already associated + * with a value, attempts to compute its value using the given mapping function and enters it into this map . + *

+ * Stores value mapped by key with specified time to live. + * Entry expires after specified time to live. + * + * @param key - map key + * @param ttl - time to live for key\value entry. + * If 0 then stores infinitely. + * @param mappingFunction the mapping function to compute a value + * @return current associated value + */ + Mono computeIfAbsentAsync(K key, Duration ttl, Function mappingFunction); /** * Stores value mapped by key with specified time to live. * Entry expires after specified time to live. diff --git a/redisson/src/main/java/org/redisson/api/RMapCacheRx.java b/redisson/src/main/java/org/redisson/api/RMapCacheRx.java index c65a81716..810391b22 100644 --- a/redisson/src/main/java/org/redisson/api/RMapCacheRx.java +++ b/redisson/src/main/java/org/redisson/api/RMapCacheRx.java @@ -26,6 +26,7 @@ import java.time.Duration; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; +import java.util.function.Function; /** *

Map-based cache with ability to set TTL for each entry via @@ -128,7 +129,20 @@ public interface RMapCacheRx extends RMapRx, RDestroyable { * @return previous associated value */ Maybe putIfAbsent(K key, V value, long ttl, TimeUnit ttlUnit, long maxIdleTime, TimeUnit maxIdleUnit); - + /** + * If the specified key is not already associated + * with a value, attempts to compute its value using the given mapping function and enters it into this map . + *

+ * Stores value mapped by key with specified time to live. + * Entry expires after specified time to live. + * + * @param key - map key + * @param ttl - time to live for key\value entry. + * If 0 then stores infinitely. + * @param mappingFunction the mapping function to compute a value + * @return current associated value + */ + Maybe computeIfAbsentAsync(K key, Duration ttl, Function mappingFunction); /** * Stores value mapped by key with specified time to live. * Entry expires after specified time to live. diff --git a/redisson/src/test/java/org/redisson/RedissonMapCacheTest.java b/redisson/src/test/java/org/redisson/RedissonMapCacheTest.java index 3802e0e42..ab22cca34 100644 --- a/redisson/src/test/java/org/redisson/RedissonMapCacheTest.java +++ b/redisson/src/test/java/org/redisson/RedissonMapCacheTest.java @@ -1470,5 +1470,21 @@ public class RedissonMapCacheTest extends BaseMapTest { map.destroy(); } + + @Test + public void testComputeIfAbsentWithTTL() throws Exception{ + RMapCache map = redisson.getMapCache("testMap"); + map.delete(); + Duration duration = Duration.ofSeconds(1); + String value = map.computeIfAbsent("key1",duration, (t1) -> "value1"); + assertThat("value1".equals(value)).isTrue(); + value = map.computeIfAbsent("key1", duration, (t1) -> "value2"); + assertThat("value2".equals(value)).isFalse(); + Thread.sleep(1100); + value = map.computeIfAbsent("key1", duration, (t1) -> "value3"); + assertThat("value3".equals(value)).isTrue(); + map.destroy(); + + } } From 2cc4f5ad79ba449158a220c6c774eb3e8f274480 Mon Sep 17 00:00:00 2001 From: Jakub Glapa Date: Wed, 18 Sep 2024 12:48:53 +0200 Subject: [PATCH 2/4] Allow to use rogrammatically configured config object Signed-off-by: Jakub Glapa --- redisson-tomcat/README.md | 2 +- .../tomcat/RedissonSessionManager.java | 30 ++++++++++++------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/redisson-tomcat/README.md b/redisson-tomcat/README.md index 4fc2d69dc..84a5e1cf3 100644 --- a/redisson-tomcat/README.md +++ b/redisson-tomcat/README.md @@ -34,7 +34,7 @@ Add `RedissonSessionManager` in global context - `tomcat/conf/context.xml` or pe * In `readMode=MEMORY` all session attributes are always stored into Redis after the end of the request regardless of the `Session.setAttribute` method invocation. It is useful in case when some objects stored in session change their own state without `Session.setAttribute` method execution. Updated attributes are removed from all other Session instances if `broadcastSessionUpdates=true` and reloaded from Redis when these attributes are requested.
- `configPath` - path to Redisson YAML config. See [configuration wiki page](https://github.com/redisson/redisson/wiki/2.-Configuration) for more details. + `configPath` - path to Redisson YAML config. See [configuration wiki page](https://github.com/redisson/redisson/wiki/2.-Configuration) for more details. In case session manager is configured programatically, a config object can be passed using the `setConfig()` method #### Shared Redisson instance diff --git a/redisson-tomcat/redisson-tomcat-9/src/main/java/org/redisson/tomcat/RedissonSessionManager.java b/redisson-tomcat/redisson-tomcat-9/src/main/java/org/redisson/tomcat/RedissonSessionManager.java index 97a86b3c7..d4262773c 100644 --- a/redisson-tomcat/redisson-tomcat-9/src/main/java/org/redisson/tomcat/RedissonSessionManager.java +++ b/redisson-tomcat/redisson-tomcat-9/src/main/java/org/redisson/tomcat/RedissonSessionManager.java @@ -52,6 +52,7 @@ public class RedissonSessionManager extends ManagerBase { protected RedissonClient redisson; private String configPath; + private Config config; private ReadMode readMode = ReadMode.REDIS; private UpdateMode updateMode = UpdateMode.DEFAULT; @@ -108,6 +109,14 @@ public class RedissonSessionManager extends ManagerBase { return configPath; } + public void setConfig(Config config) { + this.config = config; + } + + public Config getConfig() { + return config; + } + public String getKeyPrefix() { return keyPrefix; } @@ -354,19 +363,20 @@ public class RedissonSessionManager extends ManagerBase { } protected RedissonClient buildClient() throws LifecycleException { - Config config = null; - try { - config = Config.fromYAML(new File(configPath), getClass().getClassLoader()); - } catch (IOException e) { - // trying next format + if (config == null) { try { - config = Config.fromJSON(new File(configPath), getClass().getClassLoader()); - } catch (IOException e1) { - log.error("Can't parse json config " + configPath, e); - throw new LifecycleException("Can't parse yaml config " + configPath, e1); + config = Config.fromYAML(new File(configPath), getClass().getClassLoader()); + } catch (IOException e) { + // trying next format + try { + config = Config.fromJSON(new File(configPath), getClass().getClassLoader()); + } catch (IOException e1) { + log.error("Can't parse json config " + configPath, e); + throw new LifecycleException("Can't parse yaml config " + configPath, e1); + } } } - + try { return Redisson.create(config); } catch (Exception e) { From 0c5392128a35b92698c3670c421dadedc1e3edfb Mon Sep 17 00:00:00 2001 From: Jakub Glapa Date: Thu, 19 Sep 2024 12:41:44 +0200 Subject: [PATCH 3/4] copy changes to other tomcat versions Signed-off-by: Jakub Glapa --- .../tomcat/RedissonSessionManager.java | 22 ++++++++++--------- .../tomcat/RedissonSessionManager.java | 22 ++++++++++--------- .../tomcat/RedissonSessionManager.java | 22 ++++++++++--------- 3 files changed, 36 insertions(+), 30 deletions(-) diff --git a/redisson-tomcat/redisson-tomcat-10/src/main/java/org/redisson/tomcat/RedissonSessionManager.java b/redisson-tomcat/redisson-tomcat-10/src/main/java/org/redisson/tomcat/RedissonSessionManager.java index e3cf71456..89fa8296e 100644 --- a/redisson-tomcat/redisson-tomcat-10/src/main/java/org/redisson/tomcat/RedissonSessionManager.java +++ b/redisson-tomcat/redisson-tomcat-10/src/main/java/org/redisson/tomcat/RedissonSessionManager.java @@ -52,6 +52,7 @@ public class RedissonSessionManager extends ManagerBase { protected RedissonClient redisson; private String configPath; + private Config config; private ReadMode readMode = ReadMode.REDIS; private UpdateMode updateMode = UpdateMode.DEFAULT; @@ -354,19 +355,20 @@ public class RedissonSessionManager extends ManagerBase { } protected RedissonClient buildClient() throws LifecycleException { - Config config = null; - try { - config = Config.fromYAML(new File(configPath), getClass().getClassLoader()); - } catch (IOException e) { - // trying next format + if (config == null) { try { - config = Config.fromJSON(new File(configPath), getClass().getClassLoader()); - } catch (IOException e1) { - log.error("Can't parse json config " + configPath, e); - throw new LifecycleException("Can't parse yaml config " + configPath, e1); + config = Config.fromYAML(new File(configPath), getClass().getClassLoader()); + } catch (IOException e) { + // trying next format + try { + config = Config.fromJSON(new File(configPath), getClass().getClassLoader()); + } catch (IOException e1) { + log.error("Can't parse json config " + configPath, e); + throw new LifecycleException("Can't parse yaml config " + configPath, e1); + } } } - + try { return Redisson.create(config); } catch (Exception e) { diff --git a/redisson-tomcat/redisson-tomcat-7/src/main/java/org/redisson/tomcat/RedissonSessionManager.java b/redisson-tomcat/redisson-tomcat-7/src/main/java/org/redisson/tomcat/RedissonSessionManager.java index 50ea78318..8676108fa 100644 --- a/redisson-tomcat/redisson-tomcat-7/src/main/java/org/redisson/tomcat/RedissonSessionManager.java +++ b/redisson-tomcat/redisson-tomcat-7/src/main/java/org/redisson/tomcat/RedissonSessionManager.java @@ -52,6 +52,7 @@ public class RedissonSessionManager extends ManagerBase { protected RedissonClient redisson; private String configPath; + private Config config; private ReadMode readMode = ReadMode.REDIS; private UpdateMode updateMode = UpdateMode.DEFAULT; @@ -354,19 +355,20 @@ public class RedissonSessionManager extends ManagerBase { } protected RedissonClient buildClient() throws LifecycleException { - Config config = null; - try { - config = Config.fromYAML(new File(configPath), getClass().getClassLoader()); - } catch (IOException e) { - // trying next format + if (config == null) { try { - config = Config.fromJSON(new File(configPath), getClass().getClassLoader()); - } catch (IOException e1) { - log.error("Can't parse json config " + configPath, e); - throw new LifecycleException("Can't parse yaml config " + configPath, e1); + config = Config.fromYAML(new File(configPath), getClass().getClassLoader()); + } catch (IOException e) { + // trying next format + try { + config = Config.fromJSON(new File(configPath), getClass().getClassLoader()); + } catch (IOException e1) { + log.error("Can't parse json config " + configPath, e); + throw new LifecycleException("Can't parse yaml config " + configPath, e1); + } } } - + try { return Redisson.create(config); } catch (Exception e) { diff --git a/redisson-tomcat/redisson-tomcat-8/src/main/java/org/redisson/tomcat/RedissonSessionManager.java b/redisson-tomcat/redisson-tomcat-8/src/main/java/org/redisson/tomcat/RedissonSessionManager.java index 97a86b3c7..64af63371 100644 --- a/redisson-tomcat/redisson-tomcat-8/src/main/java/org/redisson/tomcat/RedissonSessionManager.java +++ b/redisson-tomcat/redisson-tomcat-8/src/main/java/org/redisson/tomcat/RedissonSessionManager.java @@ -52,6 +52,7 @@ public class RedissonSessionManager extends ManagerBase { protected RedissonClient redisson; private String configPath; + private Config config; private ReadMode readMode = ReadMode.REDIS; private UpdateMode updateMode = UpdateMode.DEFAULT; @@ -354,19 +355,20 @@ public class RedissonSessionManager extends ManagerBase { } protected RedissonClient buildClient() throws LifecycleException { - Config config = null; - try { - config = Config.fromYAML(new File(configPath), getClass().getClassLoader()); - } catch (IOException e) { - // trying next format + if (config == null) { try { - config = Config.fromJSON(new File(configPath), getClass().getClassLoader()); - } catch (IOException e1) { - log.error("Can't parse json config " + configPath, e); - throw new LifecycleException("Can't parse yaml config " + configPath, e1); + config = Config.fromYAML(new File(configPath), getClass().getClassLoader()); + } catch (IOException e) { + // trying next format + try { + config = Config.fromJSON(new File(configPath), getClass().getClassLoader()); + } catch (IOException e1) { + log.error("Can't parse json config " + configPath, e); + throw new LifecycleException("Can't parse yaml config " + configPath, e1); + } } } - + try { return Redisson.create(config); } catch (Exception e) { From 2040332fe520b9733c5447afda0ba776026bf817 Mon Sep 17 00:00:00 2001 From: Jakub Glapa Date: Thu, 19 Sep 2024 12:42:36 +0200 Subject: [PATCH 4/4] test for programmatic configuration of RedissonSessionmanager Signed-off-by: Jakub Glapa --- .../tomcat/RedissonSessionManagerTest.java | 47 +++++++++++++++++++ .../org/redisson/tomcat/TomcatServer.java | 12 ++++- .../test/webapp/META-INF/context_empty.xml | 4 ++ 3 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 redisson-tomcat/redisson-tomcat-9/src/test/webapp/META-INF/context_empty.xml diff --git a/redisson-tomcat/redisson-tomcat-9/src/test/java/org/redisson/tomcat/RedissonSessionManagerTest.java b/redisson-tomcat/redisson-tomcat-9/src/test/java/org/redisson/tomcat/RedissonSessionManagerTest.java index 5e34d55ee..f7ed0f421 100644 --- a/redisson-tomcat/redisson-tomcat-9/src/test/java/org/redisson/tomcat/RedissonSessionManagerTest.java +++ b/redisson-tomcat/redisson-tomcat-9/src/test/java/org/redisson/tomcat/RedissonSessionManagerTest.java @@ -1,9 +1,11 @@ package org.redisson.tomcat; +import org.apache.catalina.Manager; import org.apache.http.client.fluent.Executor; import org.apache.http.client.fluent.Request; import org.apache.http.cookie.Cookie; import org.apache.http.impl.client.BasicCookieStore; +import org.junit.Test; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -18,6 +20,9 @@ import java.nio.file.Paths; import java.util.Arrays; import java.util.concurrent.TimeUnit; +import static org.redisson.tomcat.RedissonSessionManager.ReadMode.MEMORY; +import static org.redisson.tomcat.RedissonSessionManager.UpdateMode.AFTER_REQUEST; + public class RedissonSessionManagerTest { public static Iterable data() { @@ -35,6 +40,48 @@ public class RedissonSessionManagerTest { Files.copy(Paths.get(basePath + contextName), Paths.get(basePath + "context.xml")); } + private Manager prepareManager() { + Config config = new Config(); + config.useSingleServer().setAddress("redis://127.0.0.1:6379"); + + RedissonSessionManager redissonSessionManager = new RedissonSessionManager(); + redissonSessionManager.setReadMode(MEMORY.name()); + redissonSessionManager.setUpdateMode(AFTER_REQUEST.name()); + redissonSessionManager.setBroadcastSessionEvents(true); + redissonSessionManager.setBroadcastSessionUpdates(true); + redissonSessionManager.setConfig(config); + + return redissonSessionManager; + } + + @Test + public void testProgrammaticManagerConfigurationUpdateTwoServers_readValue() throws Exception { + prepare("context_empty.xml"); + TomcatServer server1 = new TomcatServer("myapp", 8080, "src/test/"); + server1.setManager(prepareManager()); + TomcatServer server2 = new TomcatServer("myapp", 8081, "src/test/"); + server2.setManager(prepareManager()); + try { + server1.start(); + server2.start(); + + Executor executor = Executor.newInstance(); + BasicCookieStore cookieStore = new BasicCookieStore(); + executor.use(cookieStore); + + write(8080, executor, "test", "from_server1"); + write(8081, executor, "test", "from_server2"); + + read(8080, executor, "test", "from_server2"); + read(8081, executor, "test", "from_server2"); + + } finally { + Executor.closeIdleConnections(); + server1.stop(); + server2.stop(); + } + } + @ParameterizedTest @MethodSource("data") public void testUpdateTwoServers_readValue(String contextName) throws Exception { diff --git a/redisson-tomcat/redisson-tomcat-9/src/test/java/org/redisson/tomcat/TomcatServer.java b/redisson-tomcat/redisson-tomcat-9/src/test/java/org/redisson/tomcat/TomcatServer.java index d18978fb3..55db289b0 100644 --- a/redisson-tomcat/redisson-tomcat-9/src/test/java/org/redisson/tomcat/TomcatServer.java +++ b/redisson-tomcat/redisson-tomcat-9/src/test/java/org/redisson/tomcat/TomcatServer.java @@ -4,7 +4,9 @@ import java.net.MalformedURLException; import javax.servlet.ServletException; +import org.apache.catalina.Context; import org.apache.catalina.LifecycleException; +import org.apache.catalina.Manager; import org.apache.catalina.startup.Tomcat; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -12,6 +14,7 @@ import org.slf4j.LoggerFactory; public class TomcatServer { private Tomcat tomcat = new Tomcat(); + private Context context; private int port; private boolean isRunning; @@ -30,7 +33,14 @@ public class TomcatServer { tomcat.setPort(port); tomcat.getHost().setAppBase("."); - tomcat.addWebapp(contextPath, appBase + "/webapp"); + context = tomcat.addWebapp(contextPath, appBase + "/webapp"); + } + + void setManager(Manager manager) { + if (context.getManager() != null) { + throw new IllegalArgumentException("Manager already configured"); + } + context.setManager(manager); } /** diff --git a/redisson-tomcat/redisson-tomcat-9/src/test/webapp/META-INF/context_empty.xml b/redisson-tomcat/redisson-tomcat-9/src/test/webapp/META-INF/context_empty.xml new file mode 100644 index 000000000..dd1d6b33b --- /dev/null +++ b/redisson-tomcat/redisson-tomcat-9/src/test/webapp/META-INF/context_empty.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file