From 796eab814d93601d19af9901004df9d8f23b3c29 Mon Sep 17 00:00:00 2001 From: Jarrod Ribble Date: Mon, 20 Feb 2017 18:53:41 -0700 Subject: [PATCH] Add an option to use soft references for eviction with LocalCachedMap. --- .travis.yml | 2 +- .../org/redisson/RedissonLocalCachedMap.java | 10 ++- .../redisson/api/LocalCachedMapOptions.java | 3 +- .../org/redisson/misc/AbstractCacheMap.java | 50 +------------- .../java/org/redisson/misc/CachedValue.java | 12 ++++ .../java/org/redisson/misc/LFUCacheMap.java | 2 +- .../java/org/redisson/misc/SoftCacheMap.java | 39 +++++++++++ .../org/redisson/misc/SoftCachedValue.java | 35 ++++++++++ .../org/redisson/misc/StdCachedValue.java | 56 ++++++++++++++++ .../org/redisson/misc/NoneCacheMapTest.java | 3 + .../org/redisson/misc/SoftCacheMapTest.java | 65 +++++++++++++++++++ 11 files changed, 219 insertions(+), 58 deletions(-) create mode 100644 redisson/src/main/java/org/redisson/misc/CachedValue.java create mode 100644 redisson/src/main/java/org/redisson/misc/SoftCacheMap.java create mode 100644 redisson/src/main/java/org/redisson/misc/SoftCachedValue.java create mode 100644 redisson/src/main/java/org/redisson/misc/StdCachedValue.java create mode 100644 redisson/src/test/java/org/redisson/misc/SoftCacheMapTest.java diff --git a/.travis.yml b/.travis.yml index d911a0161..855eb09e8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -224,4 +224,4 @@ before_script: - export REDIS_VERSION="$(redis-cli INFO SERVER | sed -n 2p)" - echo $REDIS_VERSION - redis-cli SHUTDOWN NOSAVE -script: mvn -Dtest=$REDISSON_TEST -Dsurefire.rerunFailingTestsCount=5 -DargLine="-Xmx2g -DredisBinary=$REDIS_BIN/redis-server -DtravisEnv=true" -Punit-test clean test -e -X +script: mvn -Dtest=$REDISSON_TEST -Dsurefire.rerunFailingTestsCount=5 -DargLine="-Xmx2g -DredisBinary=$REDIS_BIN/redis-server -DtravisEnv=true -XX:SoftRefLRUPolicyMSPerMB=0" -Punit-test clean test -e -X diff --git a/redisson/src/main/java/org/redisson/RedissonLocalCachedMap.java b/redisson/src/main/java/org/redisson/RedissonLocalCachedMap.java index 14ae25699..731118d3c 100644 --- a/redisson/src/main/java/org/redisson/RedissonLocalCachedMap.java +++ b/redisson/src/main/java/org/redisson/RedissonLocalCachedMap.java @@ -48,12 +48,7 @@ import org.redisson.client.protocol.convertor.NumberConvertor; import org.redisson.client.protocol.decoder.ObjectMapEntryReplayDecoder; import org.redisson.client.protocol.decoder.ObjectSetReplayDecoder; import org.redisson.command.CommandAsyncExecutor; -import org.redisson.misc.Cache; -import org.redisson.misc.Hash; -import org.redisson.misc.LFUCacheMap; -import org.redisson.misc.LRUCacheMap; -import org.redisson.misc.NoneCacheMap; -import org.redisson.misc.RPromise; +import org.redisson.misc.*; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.FutureListener; @@ -215,6 +210,9 @@ public class RedissonLocalCachedMap extends RedissonMap implements R if (options.getEvictionPolicy() == EvictionPolicy.LFU) { cache = new LFUCacheMap(options.getCacheSize(), options.getTimeToLiveInMillis(), options.getMaxIdleInMillis()); } + if (options.getEvictionPolicy() == EvictionPolicy.SOFT) { + cache = new SoftCacheMap(options.getTimeToLiveInMillis(), options.getMaxIdleInMillis()); + } invalidationTopic = new RedissonTopic(commandExecutor, suffixName(name, "topic")); if (options.isInvalidateEntryOnChange()) { diff --git a/redisson/src/main/java/org/redisson/api/LocalCachedMapOptions.java b/redisson/src/main/java/org/redisson/api/LocalCachedMapOptions.java index 89a600aa5..523cc5ff5 100644 --- a/redisson/src/main/java/org/redisson/api/LocalCachedMapOptions.java +++ b/redisson/src/main/java/org/redisson/api/LocalCachedMapOptions.java @@ -25,7 +25,7 @@ import java.util.concurrent.TimeUnit; */ public class LocalCachedMapOptions { - public enum EvictionPolicy {NONE, LRU, LFU}; + public enum EvictionPolicy {NONE, LRU, LFU, SOFT}; private boolean invalidateEntryOnChange; private EvictionPolicy evictionPolicy; @@ -115,6 +115,7 @@ public class LocalCachedMapOptions { * @param evictionPolicy *

LRU - uses cache with LRU (least recently used) eviction policy. *

LFU - uses cache with LFU (least frequently used) eviction policy. + *

SOFT - uses cache with soft references. The garbage collector will evict items from the cache when the JVM is running out of memory. *

NONE - doesn't use eviction policy, but timeToLive and maxIdleTime params are still working. * @return LocalCachedMapOptions instance */ diff --git a/redisson/src/main/java/org/redisson/misc/AbstractCacheMap.java b/redisson/src/main/java/org/redisson/misc/AbstractCacheMap.java index 75fecc81c..3a1527e29 100644 --- a/redisson/src/main/java/org/redisson/misc/AbstractCacheMap.java +++ b/redisson/src/main/java/org/redisson/misc/AbstractCacheMap.java @@ -37,54 +37,6 @@ import io.netty.util.internal.PlatformDependent; */ public abstract class AbstractCacheMap implements Cache { - public static class CachedValue { - - private final Object key; - private final Object value; - - long ttl; - long maxIdleTime; - - long creationTime; - long lastAccess; - - public CachedValue(Object key, Object value, long ttl, long maxIdleTime) { - this.value = value; - this.ttl = ttl; - this.key = key; - this.maxIdleTime = maxIdleTime; - creationTime = System.currentTimeMillis(); - lastAccess = creationTime; - } - - public boolean isExpired() { - boolean result = false; - long currentTime = System.currentTimeMillis(); - if (ttl != 0 && creationTime + ttl < currentTime) { - result = true; - } - if (maxIdleTime != 0 && lastAccess + maxIdleTime < currentTime) { - result = true; - } - return result; - } - - public Object getKey() { - return key; - } - - public Object getValue() { - lastAccess = System.currentTimeMillis(); - return value; - } - - @Override - public String toString() { - return "CachedValue [key=" + key + ", value=" + value + "]"; - } - - } - final int size; final ConcurrentMap map = PlatformDependent.newConcurrentHashMap(); private final long timeToLiveInMillis; @@ -237,7 +189,7 @@ public abstract class AbstractCacheMap implements Cache { } protected CachedValue create(K key, V value, long ttl, long maxIdleTime) { - return new CachedValue(key, value, ttl, maxIdleTime); + return new StdCachedValue(key, value, ttl, maxIdleTime); } protected void onValueCreate(CachedValue entry) { diff --git a/redisson/src/main/java/org/redisson/misc/CachedValue.java b/redisson/src/main/java/org/redisson/misc/CachedValue.java new file mode 100644 index 000000000..8fb9b5d24 --- /dev/null +++ b/redisson/src/main/java/org/redisson/misc/CachedValue.java @@ -0,0 +1,12 @@ +package org.redisson.misc; + +/** + * Created by jribble on 2/20/17. + */ +public interface CachedValue { + boolean isExpired(); + + K getKey(); + + V getValue(); +} diff --git a/redisson/src/main/java/org/redisson/misc/LFUCacheMap.java b/redisson/src/main/java/org/redisson/misc/LFUCacheMap.java index 720b46088..52d795a2e 100644 --- a/redisson/src/main/java/org/redisson/misc/LFUCacheMap.java +++ b/redisson/src/main/java/org/redisson/misc/LFUCacheMap.java @@ -57,7 +57,7 @@ public class LFUCacheMap extends AbstractCacheMap { } - public static class LFUCachedValue extends CachedValue { + public static class LFUCachedValue extends StdCachedValue { Long id; long accessCount; diff --git a/redisson/src/main/java/org/redisson/misc/SoftCacheMap.java b/redisson/src/main/java/org/redisson/misc/SoftCacheMap.java new file mode 100644 index 000000000..cab8b0309 --- /dev/null +++ b/redisson/src/main/java/org/redisson/misc/SoftCacheMap.java @@ -0,0 +1,39 @@ +/** + * Copyright 2016 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.misc; + +/** + * + * @author Nikita Koksharov + * + * @param key + * @param value + */ +public class SoftCacheMap extends AbstractCacheMap { + + public SoftCacheMap(long timeToLiveInMillis, long maxIdleInMillis) { + super(0, timeToLiveInMillis, maxIdleInMillis); + } + + protected CachedValue create(K key, V value, long ttl, long maxIdleTime) { + return new SoftCachedValue(key, value, ttl, maxIdleTime); + } + + @Override + protected void onMapFull() { + } + +} diff --git a/redisson/src/main/java/org/redisson/misc/SoftCachedValue.java b/redisson/src/main/java/org/redisson/misc/SoftCachedValue.java new file mode 100644 index 000000000..7ae53a248 --- /dev/null +++ b/redisson/src/main/java/org/redisson/misc/SoftCachedValue.java @@ -0,0 +1,35 @@ +package org.redisson.misc; + +import java.lang.ref.SoftReference; + +/** + * Created by jribble on 2/20/17. + */ + +public class SoftCachedValue implements CachedValue { + StdCachedValue> value; + + public SoftCachedValue(K key, V value, long ttl, long maxIdleTime) { + this.value = new StdCachedValue<>(key, new SoftReference<>(value), ttl, maxIdleTime); + } + + @Override + public boolean isExpired() { + return value.isExpired(); + } + + @Override + public K getKey() { + return value.getKey(); + } + + @Override + public V getValue() { + return value.getValue().get(); + } + + @Override + public String toString() { + return value.toString(); + } +} diff --git a/redisson/src/main/java/org/redisson/misc/StdCachedValue.java b/redisson/src/main/java/org/redisson/misc/StdCachedValue.java new file mode 100644 index 000000000..edf2c67c7 --- /dev/null +++ b/redisson/src/main/java/org/redisson/misc/StdCachedValue.java @@ -0,0 +1,56 @@ +package org.redisson.misc; + +/** + * Created by jribble on 2/20/17. + */ + +public class StdCachedValue implements CachedValue { + + protected final K key; + protected final V value; + + long ttl; + long maxIdleTime; + + long creationTime; + long lastAccess; + + public StdCachedValue(K key, V value, long ttl, long maxIdleTime) { + this.value = value; + this.ttl = ttl; + this.key = key; + this.maxIdleTime = maxIdleTime; + creationTime = System.currentTimeMillis(); + lastAccess = creationTime; + } + + @Override + public boolean isExpired() { + boolean result = false; + long currentTime = System.currentTimeMillis(); + if (ttl != 0 && creationTime + ttl < currentTime) { + result = true; + } + if (maxIdleTime != 0 && lastAccess + maxIdleTime < currentTime) { + result = true; + } + return result; + } + + @Override + public K getKey() { + return key; + } + + @Override + public V getValue() { + lastAccess = System.currentTimeMillis(); + return value; + } + + @Override + public String toString() { + return "CachedValue [key=" + key + ", value=" + value + "]"; + } + +} diff --git a/redisson/src/test/java/org/redisson/misc/NoneCacheMapTest.java b/redisson/src/test/java/org/redisson/misc/NoneCacheMapTest.java index 63f5a58e8..744b3a4c0 100644 --- a/redisson/src/test/java/org/redisson/misc/NoneCacheMapTest.java +++ b/redisson/src/test/java/org/redisson/misc/NoneCacheMapTest.java @@ -1,7 +1,10 @@ package org.redisson.misc; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import java.util.ArrayList; +import java.util.Objects; import java.util.concurrent.TimeUnit; import org.junit.Test; diff --git a/redisson/src/test/java/org/redisson/misc/SoftCacheMapTest.java b/redisson/src/test/java/org/redisson/misc/SoftCacheMapTest.java new file mode 100644 index 000000000..788bd6e85 --- /dev/null +++ b/redisson/src/test/java/org/redisson/misc/SoftCacheMapTest.java @@ -0,0 +1,65 @@ +package org.redisson.misc; + +import org.junit.Test; + +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; + +public class SoftCacheMapTest { + + @Test + public void testMaxIdleTimeEviction() throws InterruptedException { + Cache map = new SoftCacheMap(0, 0); + map.put(1, 0, 0, TimeUnit.MILLISECONDS, 400, TimeUnit.MILLISECONDS); + assertThat(map.get(1)).isEqualTo(0); + Thread.sleep(200); + assertThat(map.get(1)).isEqualTo(0); + Thread.sleep(200); + assertThat(map.get(1)).isEqualTo(0); + Thread.sleep(200); + assertThat(map.get(1)).isEqualTo(0); + Thread.sleep(410); + assertThat(map.keySet()).isEmpty(); + } + + @Test + public void testTTLEviction() throws InterruptedException { + Cache map = new SoftCacheMap(0, 0); + map.put(1, 0, 500, TimeUnit.MILLISECONDS, 0, TimeUnit.MILLISECONDS); + assertThat(map.get(1)).isEqualTo(0); + Thread.sleep(100); + assertThat(map.get(1)).isEqualTo(0); + assertThat(map.keySet()).containsOnly(1); + Thread.sleep(500); + assertThat(map.keySet()).isEmpty(); + } + + @Test + public void testSizeEviction() { + Cache map = new SoftCacheMap(0, 0); + map.put(1, 0); + map.put(2, 0); + + assertThat(map.keySet()).containsOnly(1, 2); + map.put(3, 0); + map.put(4, 0); + + assertThat(map.keySet()).containsOnly(1, 2, 3, 4); + } + + // This test requires using -XX:SoftRefLRUPolicyMSPerMB=0 to pass + @Test + public void testSoftReferences() { + Cache map = new SoftCacheMap(0, 0); + for(int i=0;i<100000;i++) { + map.put(i, new Integer(i)); + } + + assertThat(map.values().stream().filter(Objects::nonNull).count()).isEqualTo(100000); + System.gc(); + assertThat(map.values().stream().filter(Objects::nonNull).count()).isZero(); + } + +}