This is an automated email from the ASF dual-hosted git repository. jamesfredley pushed a commit to branch feat/caffeine-cache-migration-seed in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit e01d6d8f766b75a5cfb740267f4e883df6cb5fa6 Author: James Fredley <[email protected]> AuthorDate: Fri Jul 10 12:58:01 2026 -0400 Migrate concurrent linked map cache to Caffeine Replace concurrentlinkedhashmap-lru internals with Caffeine while keeping public cache API. Assisted-by: Sisyphus:xai/grok-4.5 [gpt-coding] --- grails-cache/build.gradle | 2 +- .../cache/GrailsConcurrentLinkedMapCache.java | 27 ++-- .../GrailsConcurrentLinkedMapCacheManager.groovy | 2 +- .../GrailsConcurrentLinkedMapCacheTests.groovy | 151 +++++++++++++-------- .../guide/cache/cacheUsage/cacheConfiguration.adoc | 3 +- 5 files changed, 112 insertions(+), 73 deletions(-) diff --git a/grails-cache/build.gradle b/grails-cache/build.gradle index 5534081f00..26a70a9ecf 100644 --- a/grails-cache/build.gradle +++ b/grails-cache/build.gradle @@ -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') diff --git a/grails-cache/src/main/groovy/grails/plugin/cache/GrailsConcurrentLinkedMapCache.java b/grails-cache/src/main/groovy/grails/plugin/cache/GrailsConcurrentLinkedMapCache.java index efa156ab10..471492d3c0 100644 --- a/grails-cache/src/main/groovy/grails/plugin/cache/GrailsConcurrentLinkedMapCache.java +++ b/grails-cache/src/main/groovy/grails/plugin/cache/GrailsConcurrentLinkedMapCache.java @@ -20,10 +20,13 @@ package grails.plugin.cache; 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; @@ -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; /** @@ -49,11 +53,10 @@ public class GrailsConcurrentLinkedMapCache implements GrailsCache { 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(); } /** @@ -67,7 +70,7 @@ public class GrailsConcurrentLinkedMapCache implements GrailsCache { } public final long getCapacity() { - return this.store.capacity(); + return this.capacity; } @Override @@ -81,6 +84,7 @@ public class GrailsConcurrentLinkedMapCache implements GrailsCache { } public final int getSize() { + this.caffeineStore.cleanUp(); return this.store.size(); } @@ -114,7 +118,12 @@ public class GrailsConcurrentLinkedMapCache implements GrailsCache { } 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 @@ -123,7 +132,7 @@ public class GrailsConcurrentLinkedMapCache implements GrailsCache { } 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); } diff --git a/grails-cache/src/main/groovy/grails/plugin/cache/GrailsConcurrentLinkedMapCacheManager.groovy b/grails-cache/src/main/groovy/grails/plugin/cache/GrailsConcurrentLinkedMapCacheManager.groovy index 2a73bf73a7..55012c8d64 100644 --- a/grails-cache/src/main/groovy/grails/plugin/cache/GrailsConcurrentLinkedMapCacheManager.groovy +++ b/grails-cache/src/main/groovy/grails/plugin/cache/GrailsConcurrentLinkedMapCacheManager.groovy @@ -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 */ diff --git a/grails-cache/src/test/groovy/grails/plugin/cache/GrailsConcurrentLinkedMapCacheTests.groovy b/grails-cache/src/test/groovy/grails/plugin/cache/GrailsConcurrentLinkedMapCacheTests.groovy index 3f27165849..a007d64875 100644 --- a/grails-cache/src/test/groovy/grails/plugin/cache/GrailsConcurrentLinkedMapCacheTests.groovy +++ b/grails-cache/src/test/groovy/grails/plugin/cache/GrailsConcurrentLinkedMapCacheTests.groovy @@ -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.') } - @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) - 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 } } diff --git a/grails-doc/src/en/guide/cache/cacheUsage/cacheConfiguration.adoc b/grails-doc/src/en/guide/cache/cacheUsage/cacheConfiguration.adoc index fce48ea547..7ba9a4ca51 100644 --- a/grails-doc/src/en/guide/cache/cacheUsage/cacheConfiguration.adoc +++ b/grails-doc/src/en/guide/cache/cacheUsage/cacheConfiguration.adoc @@ -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 @@ -61,4 +61,3 @@ grails: maps: maxCapacity: 6000 ---- -
