From 8d5fba196774f1a5ecdbe8e02e0c49f229ba36e7 Mon Sep 17 00:00:00 2001 From: Nikita Date: Wed, 31 Aug 2016 14:51:06 +0300 Subject: [PATCH] LRU and LFU caches implemented. #592 --- .../org/redisson/misc/AbstractCacheMap.java | 524 ++++++++++++++++++ .../main/java/org/redisson/misc/Cache.java | 32 ++ .../java/org/redisson/misc/LFUCacheMap.java | 134 +++++ .../java/org/redisson/misc/LRUCacheMap.java | 68 +++ .../java/org/redisson/misc/NoneCacheMap.java | 48 ++ .../org/redisson/misc/LFUCacheMapTest.java | 78 +++ .../org/redisson/misc/LRUCacheMapTest.java | 73 +++ .../org/redisson/misc/NoneCacheMapTest.java | 51 ++ 8 files changed, 1008 insertions(+) create mode 100644 redisson/src/main/java/org/redisson/misc/AbstractCacheMap.java create mode 100644 redisson/src/main/java/org/redisson/misc/Cache.java create mode 100644 redisson/src/main/java/org/redisson/misc/LFUCacheMap.java create mode 100644 redisson/src/main/java/org/redisson/misc/LRUCacheMap.java create mode 100644 redisson/src/main/java/org/redisson/misc/NoneCacheMap.java create mode 100644 redisson/src/test/java/org/redisson/misc/LFUCacheMapTest.java create mode 100644 redisson/src/test/java/org/redisson/misc/LRUCacheMapTest.java create mode 100644 redisson/src/test/java/org/redisson/misc/NoneCacheMapTest.java diff --git a/redisson/src/main/java/org/redisson/misc/AbstractCacheMap.java b/redisson/src/main/java/org/redisson/misc/AbstractCacheMap.java new file mode 100644 index 00000000000..3355f67b401 --- /dev/null +++ b/redisson/src/main/java/org/redisson/misc/AbstractCacheMap.java @@ -0,0 +1,524 @@ +/** + * 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; + +import java.util.AbstractCollection; +import java.util.AbstractMap.SimpleEntry; +import java.util.AbstractSet; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; + +import io.netty.util.internal.PlatformDependent; + +/** + * + * @author Nikita Koksharov + * + * @param + * @param + */ +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; + long accessCount; + + 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 void addAccessCount(long value) { + accessCount += value; + } + + public Long getAccessCount() { + return accessCount; + } + + public Object getKey() { + return key; + } + + public Object getValue() { + lastAccess = System.currentTimeMillis(); + return value; + } + + @Override + public String toString() { + return "CachedValue [key=" + key + ", value=" + value + ", accessCount=" + accessCount + "]"; + } + + } + + final int size; + final ConcurrentMap map = PlatformDependent.newConcurrentHashMap(); + private final long timeToLiveInMillis; + private final long maxIdleInMillis; + + public AbstractCacheMap(int size, long timeToLiveInMillis, long maxIdleInMillis) { + if (size < 0) { + throw new IllegalArgumentException("Size can't be " + size); + } + this.size = size; + this.maxIdleInMillis = maxIdleInMillis; + this.timeToLiveInMillis = timeToLiveInMillis; + } + + protected void onValueRead(CachedValue value) { + + } + + protected void onValueRemove(CachedValue value) { + + } + + + /* + * (non-Javadoc) + * @see java.util.Map#size() + */ + @Override + public int size() { + return map.size(); + } + + /* + * (non-Javadoc) + * @see java.util.Map#isEmpty() + */ + @Override + public boolean isEmpty() { + return map.isEmpty(); + } + + /* + * (non-Javadoc) + * @see java.util.Map#containsKey(java.lang.Object) + */ + @Override + public boolean containsKey(Object key) { + if (key == null) { + throw new NullPointerException(); + } + + CachedValue entry = map.get(key); + if (entry == null) { + return false; + } + if (entry.isExpired()) { + if (map.remove(key, entry)) { + onValueRemove(entry); + return false; + } + return containsKey(key); + } + return true; + } + + /* + * (non-Javadoc) + * @see java.util.Map#containsValue(java.lang.Object) + */ + @Override + public boolean containsValue(Object value) { + if (value == null) { + throw new NullPointerException(); + } + + for (Map.Entry entry : map.entrySet()) { + CachedValue cachedValue = entry.getValue(); + if (cachedValue.getValue().equals(value)) { + if (cachedValue.isExpired()) { + if (map.remove(cachedValue.getKey(), cachedValue)) { + onValueRemove(cachedValue); + } + } else { + readValue(cachedValue); + return true; + } + } + } + return false; + } + + /* + * (non-Javadoc) + * @see java.util.Map#get(java.lang.Object) + */ + @Override + public V get(Object key) { + if (key == null) { + throw new NullPointerException(); + } + + CachedValue entry = map.get(key); + if (entry == null) { + return null; + } + if (entry.isExpired()) { + if (map.remove(key, entry)) { + onValueRemove(entry); + return null; + } + return get(key); + } + return readValue(entry); + } + + @SuppressWarnings("unchecked") + protected V readValue(CachedValue entry) { + onValueRead(entry); + return (V) entry.getValue(); + } + + /* + * (non-Javadoc) + * @see java.util.Map#put(java.lang.Object, java.lang.Object) + */ + @Override + public V put(K key, V value) { + return put(key, value, timeToLiveInMillis, TimeUnit.MILLISECONDS, maxIdleInMillis, TimeUnit.MILLISECONDS); + } + + @SuppressWarnings("unchecked") + @Override + public V put(K key, V value, long ttl, TimeUnit ttlUnit, long maxIdleTime, TimeUnit maxIdleUnit) { + CachedValue entry = new CachedValue(key, value, ttlUnit.toMillis(ttl), maxIdleUnit.toMillis(maxIdleTime)); + if (isFull(key)) { + if (!removeExpiredEntries()) { + onMapFull(); + } + } + onValueCreate(entry); + CachedValue prevCachedValue = map.put(key, entry); + if (prevCachedValue != null) { + onValueRemove(prevCachedValue); + if (!prevCachedValue.isExpired()) { + return (V) prevCachedValue.getValue(); + } + } + return null; + } + + protected void onValueCreate(CachedValue entry) { + } + + private boolean removeExpiredEntries() { + boolean removed = false; + // TODO optimize + for (CachedValue value : map.values()) { + if (value.isExpired()) { + if (map.remove(value.getKey(), value)) { + onValueRemove(value); + removed = true; + } + } + } + return removed; + } + + protected abstract void onMapFull(); + + boolean isFull() { + if (size == 0) { + return false; + } + return map.size() >= size; + } + + private boolean isFull(K key) { + if (size == 0) { + return false; + } + if (map.size() >= size) { + return !map.containsKey(key); + } + return false; + } + + /* + * (non-Javadoc) + * @see java.util.Map#remove(java.lang.Object) + */ + @SuppressWarnings("unchecked") + @Override + public V remove(Object key) { + CachedValue entry = map.remove(key); + if (entry != null) { + onValueRemove(entry); + if (!entry.isExpired()) { + return (V) entry.getValue(); + } + } + return null; + } + + /* + * (non-Javadoc) + * @see java.util.Map#putAll(java.util.Map) + */ + @Override + public void putAll(Map m) { + removeExpiredEntries(); + for (Map.Entry entry : m.entrySet()) { + put(entry.getKey(), entry.getValue()); + } + } + + /* + * (non-Javadoc) + * @see java.util.Map#clear() + */ + @Override + public void clear() { + map.clear(); + } + + /* + * (non-Javadoc) + * @see java.util.Map#keySet() + */ + @Override + public Set keySet() { + removeExpiredEntries(); + return new KeySet(); + } + + /* + * (non-Javadoc) + * @see java.util.Map#values() + */ + @Override + public Collection values() { + removeExpiredEntries(); + return new Values(); + } + + /* + * (non-Javadoc) + * @see java.util.Map#entrySet() + */ + @Override + public Set> entrySet() { + removeExpiredEntries(); + return new EntrySet(); + } + + abstract class MapIterator implements Iterator { + + final Iterator> keyIterator = map.entrySet().iterator(); + + Map.Entry mapEntry; + + @Override + public boolean hasNext() { + if (mapEntry != null) { + return true; + } + mapEntry = null; + while (keyIterator.hasNext()) { + Map.Entry entry = keyIterator.next(); + if (entry.getValue().isExpired()) { + continue; + } + mapEntry = entry; + break; + } + return mapEntry != null; + } + + } + + final class KeySet extends AbstractSet { + + @Override + public Iterator iterator() { + return new MapIterator() { + @Override + public K next() { + if (mapEntry == null) { + throw new NoSuchElementException(); + } + + K key = mapEntry.getKey(); + mapEntry = null; + return key; + } + + @Override + public void remove() { + if (mapEntry == null) { + throw new IllegalStateException(); + } + map.remove(mapEntry.getKey()); + mapEntry = null; + } + }; + } + + @Override + public boolean contains(Object o) { + return AbstractCacheMap.this.containsKey(o); + } + + @Override + public boolean remove(Object o) { + return AbstractCacheMap.this.remove(o) != null; + } + + @Override + public int size() { + return AbstractCacheMap.this.size(); + } + + @Override + public void clear() { + AbstractCacheMap.this.clear(); + } + + } + + final class Values extends AbstractCollection { + + @Override + public Iterator iterator() { + return new MapIterator() { + @Override + public V next() { + if (mapEntry == null) { + throw new NoSuchElementException(); + } + + V value = readValue(mapEntry.getValue()); + mapEntry = null; + return value; + } + + @Override + public void remove() { + if (mapEntry == null) { + throw new IllegalStateException(); + } + map.remove(mapEntry.getKey(), mapEntry.getValue()); + mapEntry = null; + } + }; + } + + @Override + public boolean contains(Object o) { + return AbstractCacheMap.this.containsValue(o); + } + + @Override + public int size() { + return AbstractCacheMap.this.size(); + } + + @Override + public void clear() { + AbstractCacheMap.this.clear(); + } + + } + + final class EntrySet extends AbstractSet> { + + public final Iterator> iterator() { + return new MapIterator>() { + @Override + public Map.Entry next() { + if (mapEntry == null) { + throw new NoSuchElementException(); + } + + SimpleEntry result = new SimpleEntry(mapEntry.getKey(), readValue(mapEntry.getValue())); + mapEntry = null; + return result; + } + + @Override + public void remove() { + if (mapEntry == null) { + throw new IllegalStateException(); + } + map.remove(mapEntry.getKey(), mapEntry.getValue()); + mapEntry = null; + } + }; + } + + public final boolean contains(Object o) { + if (!(o instanceof Map.Entry)) + return false; + Map.Entry e = (Map.Entry) o; + Object key = e.getKey(); + V value = get(key); + return value != null && value.equals(e); + } + + public final boolean remove(Object o) { + if (o instanceof Map.Entry) { + Map.Entry e = (Map.Entry) o; + Object key = e.getKey(); + Object value = e.getValue(); + return AbstractCacheMap.this.map.remove(key, value); + } + return false; + } + + public final int size() { + return AbstractCacheMap.this.size(); + } + + public final void clear() { + AbstractCacheMap.this.clear(); + } + + } + +} diff --git a/redisson/src/main/java/org/redisson/misc/Cache.java b/redisson/src/main/java/org/redisson/misc/Cache.java new file mode 100644 index 00000000000..f3a64c4ca5b --- /dev/null +++ b/redisson/src/main/java/org/redisson/misc/Cache.java @@ -0,0 +1,32 @@ +/** + * 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; + +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * + * @author Nikita Koksharov + * + * @param + * @param + */ +public interface Cache extends Map { + + V put(K key, V value, long ttl, TimeUnit ttlUnit, long maxIdleTime, TimeUnit maxIdleUnit); + +} diff --git a/redisson/src/main/java/org/redisson/misc/LFUCacheMap.java b/redisson/src/main/java/org/redisson/misc/LFUCacheMap.java new file mode 100644 index 00000000000..107f435fd92 --- /dev/null +++ b/redisson/src/main/java/org/redisson/misc/LFUCacheMap.java @@ -0,0 +1,134 @@ +/** + * 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; + +import java.util.Map; +import java.util.concurrent.ConcurrentNavigableMap; +import java.util.concurrent.ConcurrentSkipListMap; + +/** + * LFU (least frequently used) cache. + * + * @author Nikita Koksharov + * + * @param + * @param + */ +public class LFUCacheMap extends AbstractCacheMap { + + public static class MapKey implements Comparable { + + private Long accessCount; + private CachedValue cachedValue; + + public MapKey(Long accessCount, CachedValue cachedValue) { + super(); + this.accessCount = accessCount; + this.cachedValue = cachedValue; + } + + @Override + public int compareTo(MapKey o) { + int compare = accessCount.compareTo(o.accessCount); + if (compare == 0) { + if (!cachedValue.equals(o.cachedValue)) { + return ((Integer)cachedValue.hashCode()).compareTo(o.cachedValue.hashCode()); + } + return 0; + } + return compare; + } + + @Override + public String toString() { + return "MapKey [accessCount=" + accessCount + "]"; + } + + } + + private final ConcurrentNavigableMap accessMap = new ConcurrentSkipListMap(); + + public LFUCacheMap(int size, long timeToLiveInMillis, long maxIdleInMillis) { + super(size, timeToLiveInMillis, maxIdleInMillis); + } + + @Override + protected void onValueCreate(CachedValue value) { + MapKey key = toKey(value); + accessMap.put(key, value); + } + + @Override + protected void onValueRead(CachedValue value) { + addAccessCount(value, 1); + } + + private MapKey toKey(CachedValue value) { + return new MapKey(value.getAccessCount(), value); + } + + @Override + protected void onValueRemove(CachedValue value) { + MapKey key = toKey(value); + if (accessMap.remove(key) == null) { + throw new IllegalStateException(); + } + } + + private void addAccessCount(CachedValue value, long count) { + synchronized (value) { + if (count < 0 && value.getAccessCount() == 0) { + return; + } + + MapKey key = toKey(value); + if (accessMap.remove(key) == null) { + throw new IllegalStateException(); + } + + if (count < 0) { + count = -Math.min(value.getAccessCount(), -count); + } + value.addAccessCount(count); + + key = toKey(value); + accessMap.put(key, value); + } + } + + @Override + protected void onMapFull() { + Map.Entry entry = accessMap.pollFirstEntry(); + map.remove(entry.getValue().getKey(), entry.getValue()); + + if (entry.getValue().getAccessCount() == 0) { + return; + } + + // TODO optimize + // decrease all values + for (CachedValue value : accessMap.values()) { + addAccessCount(value, -entry.getValue().getAccessCount()); + } + } + + @Override + public void clear() { + accessMap.clear(); + super.clear(); + } + +} diff --git a/redisson/src/main/java/org/redisson/misc/LRUCacheMap.java b/redisson/src/main/java/org/redisson/misc/LRUCacheMap.java new file mode 100644 index 00000000000..c47ab0100f1 --- /dev/null +++ b/redisson/src/main/java/org/redisson/misc/LRUCacheMap.java @@ -0,0 +1,68 @@ +/** + * 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; + +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * LRU (least recently used) cache. + * + * @author Nikita Koksharov + * + * @param + * @param + */ +public class LRUCacheMap extends AbstractCacheMap { + + private final Queue queue = new ConcurrentLinkedQueue(); + + public LRUCacheMap(int size, long timeToLiveInMillis, long maxIdleInMillis) { + super(size, timeToLiveInMillis, maxIdleInMillis); + } + + @Override + protected void onValueCreate(CachedValue value) { + queue.add(value); + } + + @Override + protected void onValueRemove(CachedValue value) { + queue.remove(value); + } + + @Override + protected void onValueRead(CachedValue value) { + // move value to tail of queue + synchronized (value) { + queue.remove(value); + queue.add(value); + } + } + + @Override + protected void onMapFull() { + CachedValue value = queue.poll(); + map.remove(value.getKey(), value); + } + + @Override + public void clear() { + queue.clear(); + super.clear(); + } + +} diff --git a/redisson/src/main/java/org/redisson/misc/NoneCacheMap.java b/redisson/src/main/java/org/redisson/misc/NoneCacheMap.java new file mode 100644 index 00000000000..303d597c20e --- /dev/null +++ b/redisson/src/main/java/org/redisson/misc/NoneCacheMap.java @@ -0,0 +1,48 @@ +/** + * 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; + +import java.util.concurrent.TimeUnit; + +/** + * + * @author Nikita Koksharov + * + * @param + * @param + */ +public class NoneCacheMap extends AbstractCacheMap { + + public NoneCacheMap(long timeToLiveInMillis, long maxIdleInMillis) { + super(0, timeToLiveInMillis, maxIdleInMillis); + } + + @Override + public V put(K key, V value, long ttl, TimeUnit ttlUnit, long maxIdleTime, TimeUnit maxIdleUnit) { + CachedValue entry = new CachedValue(key, value, ttlUnit.toMillis(ttl), maxIdleUnit.toMillis(maxIdleTime)); + onValueCreate(entry); + CachedValue prevCachedValue = map.put(key, entry); + if (prevCachedValue != null && !prevCachedValue.isExpired()) { + return (V) prevCachedValue.getValue(); + } + return null; + } + + @Override + protected void onMapFull() { + } + +} diff --git a/redisson/src/test/java/org/redisson/misc/LFUCacheMapTest.java b/redisson/src/test/java/org/redisson/misc/LFUCacheMapTest.java new file mode 100644 index 00000000000..f272cc36694 --- /dev/null +++ b/redisson/src/test/java/org/redisson/misc/LFUCacheMapTest.java @@ -0,0 +1,78 @@ +package org.redisson.misc; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +public class LFUCacheMapTest { + + @Test + public void testMaxIdleTimeEviction() throws InterruptedException { + Cache map = new LFUCacheMap(2, 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 LFUCacheMap(2, 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 testSizeLFUEviction() throws InterruptedException { + Cache map = new LFUCacheMap(3, 0, 0); + + map.put(1, 0); + map.put(2, 0); + map.put(6, 0); + + map.get(1); + map.put(3, 0); + + assertThat(map.keySet()).containsOnly(3, 1, 6); + + map.get(1); + map.put(4, 0); + + assertThat(map.keySet()).contains(4, 1).hasSize(3); + } + + @Test + public void testSizeEviction() throws InterruptedException { + Cache map = new LFUCacheMap(2, 0, 0); + map.put(1, 0); + map.put(2, 0); + + assertThat(map.keySet()).containsOnly(1, 2); + + map.put(3, 0); + + assertThat(map.keySet()).contains(3).hasSize(2); + + map.put(4, 0); + + assertThat(map.keySet()).contains(4).hasSize(2); + + map.put(5, 0); + + assertThat(map.keySet()).contains(5).hasSize(2); + } + +} diff --git a/redisson/src/test/java/org/redisson/misc/LRUCacheMapTest.java b/redisson/src/test/java/org/redisson/misc/LRUCacheMapTest.java new file mode 100644 index 00000000000..53082741648 --- /dev/null +++ b/redisson/src/test/java/org/redisson/misc/LRUCacheMapTest.java @@ -0,0 +1,73 @@ +package org.redisson.misc; + +import static org.assertj.core.api.Assertions.*; + +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +public class LRUCacheMapTest { + + @Test + public void testMaxIdleTimeEviction() throws InterruptedException { + Cache map = new LRUCacheMap(2, 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 LRUCacheMap(2, 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 testSizeLRUEviction() throws InterruptedException { + Cache map = new LRUCacheMap(3, 0, 0); + map.put(1, 0); + map.put(2, 0); + map.put(5, 0); + + map.get(1); + map.put(3, 0); + + assertThat(map.keySet()).containsOnly(3, 1, 5); + + map.get(1); + map.put(4, 0); + + assertThat(map.keySet()).containsOnly(4, 1, 3); + } + + @Test + public void testSizeEviction() throws InterruptedException { + Cache map = new LRUCacheMap(2, 0, 0); + map.put(1, 0); + map.put(2, 0); + + assertThat(map.keySet()).containsOnly(1, 2); + + map.put(3, 0); + + assertThat(map.keySet()).containsOnly(3, 2); + + map.put(4, 0); + + assertThat(map.keySet()).containsOnly(4, 3); + } + +} diff --git a/redisson/src/test/java/org/redisson/misc/NoneCacheMapTest.java b/redisson/src/test/java/org/redisson/misc/NoneCacheMapTest.java new file mode 100644 index 00000000000..63f5a58e861 --- /dev/null +++ b/redisson/src/test/java/org/redisson/misc/NoneCacheMapTest.java @@ -0,0 +1,51 @@ +package org.redisson.misc; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +public class NoneCacheMapTest { + + @Test + public void testMaxIdleTimeEviction() throws InterruptedException { + Cache map = new NoneCacheMap(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 NoneCacheMap(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 NoneCacheMap(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); + } + +}