Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion grails-cache/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ dependencies {

api project(':grails-gsp')
api "org.codehaus.gpars:gpars:$gparsVersion"
api "com.googlecode.concurrentlinkedhashmap:concurrentlinkedhashmap-lru:$concurrentlinkedhashmapLruVersion"
api 'com.github.ben-manes.caffeine:caffeine'

compileOnly project(':grails-domain-class')

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,13 @@

import java.io.Serializable;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Optional;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentMap;

import com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;

import org.springframework.cache.support.SimpleValueWrapper;

Expand All @@ -35,7 +38,8 @@ public class GrailsConcurrentLinkedMapCache implements GrailsCache {
private static final Object NULL_HOLDER = new NullHolder();
private String name;
private long capacity;
private final ConcurrentLinkedHashMap<Object, Object> store;
private final Cache<Object, Object> caffeineStore;
private final ConcurrentMap<Object, Object> store;
private final boolean allowNullValues;

/**
Expand All @@ -49,11 +53,10 @@ public GrailsConcurrentLinkedMapCache(String name, long capacity, boolean allowN
this.name = name;
this.capacity = capacity;
this.allowNullValues = allowNullValues;
// Workaround: using explicit type arguments to prevent groovydoc error (#53)
// Replace with diamond operator once a fix for GROOVY-8628 is included in groovy dependency
this.store = new ConcurrentLinkedHashMap.Builder<>()
.maximumWeightedCapacity(capacity)
this.caffeineStore = Caffeine.newBuilder()
.maximumSize(capacity)
.build();
this.store = caffeineStore.asMap();
}

/**
Expand All @@ -67,7 +70,7 @@ public GrailsConcurrentLinkedMapCache(String name, long capacity) {
}

public final long getCapacity() {
return this.store.capacity();
return this.capacity;
}

@Override
Expand All @@ -81,6 +84,7 @@ public final ConcurrentMap<Object, Object> getNativeCache() {
}

public final int getSize() {
this.caffeineStore.cleanUp();
return this.store.size();
}

Expand Down Expand Up @@ -114,7 +118,12 @@ public Collection<Object> getAllKeys() {
}

public Collection<Object> getHottestKeys() {
return this.store.descendingKeySet();
Optional<com.github.benmanes.caffeine.cache.Policy.Eviction<Object, Object>> eviction = this.caffeineStore.policy().eviction();
if (eviction.isPresent()) {
int limit = capacity > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) capacity;
return new LinkedHashSet<>(eviction.get().hottest(limit).keySet());
}
return this.store.keySet();
}

@Override
Expand All @@ -123,7 +132,7 @@ public void put(Object key, Object value) {
}

public ValueWrapper putIfAbsent(Object key, Object value) {
Object existing = this.store.putIfAbsent(key, value);
Object existing = this.store.putIfAbsent(key, toStoreValue(value));
return toWrapper(existing);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import org.springframework.cache.Cache
import org.grails.plugin.cache.GrailsCacheManager

/**
* Based on com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap.
* Creates bounded Grails cache instances.
*
* @author Jakob Drangmeister
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,107 +18,138 @@
*/
package grails.plugin.cache

import java.util.concurrent.ConcurrentMap

import org.springframework.cache.support.SimpleValueWrapper
import com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap
import org.junit.Test
import spock.lang.Specification

/**
* @author Jakob Drangmeister
*/
class GrailsConcurrentLinkedMapCacheTests {

@Test
void testCreateCache() {
GrailsConcurrentLinkedMapCache smallCache = new GrailsConcurrentLinkedMapCache("smallCache", 1000)
class GrailsConcurrentLinkedMapCacheTests extends Specification {

void 'creates caches with configured capacity and null value policy'() {
when:
GrailsConcurrentLinkedMapCache smallCache = new GrailsConcurrentLinkedMapCache('smallCache', 1000)

then:
smallCache.name == 'smallCache'
smallCache.nativeCache instanceof ConcurrentMap
smallCache.capacity == 1000
smallCache.allowNullValues

when:
GrailsConcurrentLinkedMapCache bigCache = new GrailsConcurrentLinkedMapCache('bigCache', 5000000, false)

then:
bigCache.name == 'bigCache'
bigCache.nativeCache instanceof ConcurrentMap
bigCache.capacity == 5000000
!bigCache.allowNullValues
}

assert smallCache.getName() == "smallCache"
assert smallCache.getNativeCache() instanceof ConcurrentLinkedHashMap
assert smallCache.getCapacity() == 1000
assert smallCache.isAllowNullValues() == true
void 'exposes Caffeine native cache as concurrent map'() {
given:
GrailsConcurrentLinkedMapCache cache = new GrailsConcurrentLinkedMapCache('cache', 10, true)

GrailsConcurrentLinkedMapCache bigCache = new GrailsConcurrentLinkedMapCache("bigCache", 5000000, false)
when:
cache.put("key", "value")

assert bigCache.getName() == "bigCache"
assert bigCache.getNativeCache() instanceof ConcurrentLinkedHashMap
assert bigCache.getCapacity() == 5000000
assert bigCache.isAllowNullValues() == false
then:
cache.nativeCache.get('key') == 'value'
cache.nativeCache.getClass().name.startsWith('com.github.benmanes.caffeine.cache.')
Comment on lines +58 to +60
}

@Test
void testPutAndGet() {
GrailsConcurrentLinkedMapCache cache = new GrailsConcurrentLinkedMapCache("cache", 1000, true)
void 'puts and gets cache entries'() {
given:
GrailsConcurrentLinkedMapCache cache = new GrailsConcurrentLinkedMapCache('cache', 1000, true)

cache.put("key", "value");
when:
cache.put('key', 'value')

assert cache.getSize() == 1
then:
cache.size == 1
GrailsValueWrapper value = cache.get("key")
assert value.get().equals("value")
value.get() == 'value'
}

@Test
void testPutIfAbsent() {
GrailsConcurrentLinkedMapCache cache = new GrailsConcurrentLinkedMapCache("cache", 1000, true)
cache.put("key", "value")
cache.putIfAbsent("key", "value") instanceof SimpleValueWrapper
assert cache.getSize() == 1
void 'putIfAbsent keeps existing cache value'() {
given:
GrailsConcurrentLinkedMapCache cache = new GrailsConcurrentLinkedMapCache('cache', 1000, true)
cache.put('key', 'value')

expect:
cache.putIfAbsent('key', 'value') instanceof SimpleValueWrapper
cache.size == 1
}

@Test
void testEvict() {
GrailsConcurrentLinkedMapCache cache = new GrailsConcurrentLinkedMapCache("cache", 10, true)
cache.put("key", "value");
assert cache.getSize() == 1
void 'evicts cache entries'() {
given:
GrailsConcurrentLinkedMapCache cache = new GrailsConcurrentLinkedMapCache('cache', 10, true)
cache.put('key', 'value')

cache.evict("key")
assert cache.getSize() == 0
expect:
cache.size == 1

when:
cache.evict('key')

then:
cache.size == 0
}

@Test
void testCacheCapacity() {
GrailsConcurrentLinkedMapCache cache = new GrailsConcurrentLinkedMapCache("cache", 1000, true)
assert cache.getCapacity() == 1000
void 'limits cache size to configured capacity'() {
given:
GrailsConcurrentLinkedMapCache cache = new GrailsConcurrentLinkedMapCache('cache', 1000, true)

for(int i = 0; i < 2000; i++) {
cache.put(i, i)
when:
for (int i = 0; i < 2000; i++) {
cache.put(i, i)
}

assert cache.getSize() == 1000
then:
cache.capacity == 1000
cache.size == 1000
}

@Test
void testCacheGetHottestKeys() {
GrailsConcurrentLinkedMapCache cache = new GrailsConcurrentLinkedMapCache("cache", 10, true)
void 'returns hottest keys from cache eviction policy'() {
given:
GrailsConcurrentLinkedMapCache cache = new GrailsConcurrentLinkedMapCache('cache', 10, true)

for(int i = 0; i < 10; i++) {
cache.put(i, i);
for (int i = 0; i < 10; i++) {
cache.put(i, i)
}

when:
cache.get(1)
cache.get(2)

Comment on lines +123 to 126
assert cache.getHottestKeys()[0] == 2
assert cache.getHottestKeys()[1] == 1
then:
cache.hottestKeys.containsAll([1, 2])

for(int i = 10; i < 19; i++) {
cache.put(i, i);
when:
for (int i = 10; i < 19; i++) {
cache.put(i, i)
}

assert cache.getHottestKeys()[cache.getSize()-1] == 2

then:
cache.hottestKeys.contains(2)
}

@Test
void testClear() {
GrailsConcurrentLinkedMapCache cache = new GrailsConcurrentLinkedMapCache("cache", 1000, true)
assert cache.getCapacity() == 1000
void 'clears all cache entries'() {
given:
GrailsConcurrentLinkedMapCache cache = new GrailsConcurrentLinkedMapCache('cache', 1000, true)
cache.put('key', 'value')

cache.put("key", "value")
assert cache.getSize() == 1
expect:
cache.capacity == 1000
cache.size == 1

when:
cache.clear()

assert cache.getSize() == 0
then:
cache.size == 0
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ There are a few configuration options for the plugin; these are specified in
*Property*,*Default*,*Description*
grails.cache.enabled,`true`,Whether to enable the plugin
grails.cache.clearAtStartup,`false`,Whether to clear all caches at startup
grails.cache.cacheManager,GrailsConcurrentMapCacheManager,Cache Manager to use. Default cache manager uses Spring Frameworks ConcurrentMapCache which might grow limitless. If you cannot predict how many cache entries you are going to generate use "GrailsConcurrentLinkedMapCacheManager" instead which uses com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap and limits by default to 10000 entries per cache.
grails.cache.cacheManager,GrailsConcurrentMapCacheManager,Cache Manager to use. Default cache manager uses Spring Frameworks ConcurrentMapCache which might grow limitless. If you cannot predict how many cache entries you are going to generate use "GrailsConcurrentLinkedMapCacheManager" instead. In Grails 8.1 this manager keeps the public Grails cache API while using Caffeine-backed bounded caches; the old concurrentlinkedhashmap-lru dependency path is deprecated.
|===

The cache implementation used by this plugin is very simple, so there aren't
Expand Down Expand Up @@ -61,4 +61,3 @@ grails:
maps:
maxCapacity: 6000
----

Loading