diff --git a/src/main/java/org/mybatis/caches/ehcache/AbstractEhcacheCache.java b/src/main/java/org/mybatis/caches/ehcache/AbstractEhcacheCache.java index 090d127..cbeb814 100644 --- a/src/main/java/org/mybatis/caches/ehcache/AbstractEhcacheCache.java +++ b/src/main/java/org/mybatis/caches/ehcache/AbstractEhcacheCache.java @@ -49,7 +49,7 @@ public abstract class AbstractEhcacheCache implements Cache { * Instantiates a new abstract ehcache cache. * * @param id - * the chache id (namespace) + * the cache id (namespace) */ public AbstractEhcacheCache(final String id) { if (id == null) { @@ -200,6 +200,47 @@ public void setMaxEntriesLocalDisk(long maxEntriesLocalDisk) { cache.getCacheConfiguration().setMaxEntriesLocalDisk(maxEntriesLocalDisk); } + /** + * Sets the maximum bytes to be used for the disk tier. When greater than zero the cache will overflow to disk when + * the heap tier is full. + *
+ * In Ehcache 2, {@code maxBytesLocalDisk} and {@code maxEntriesLocalDisk} are mutually exclusive, and the disk pool + * type cannot be changed on a running cache instance. This method therefore removes and recreates the underlying + * cache with a fresh {@link net.sf.ehcache.config.CacheConfiguration} that applies the requested byte limit while + * preserving the other settings (TTI, TTL, heap size, eviction policy) from the current configuration. + *
+ * + * @param maxBytesLocalDisk + * the maximum number of bytes to allocate on disk. 0 means no disk tier (heap-only). + */ + public void setMaxBytesLocalDisk(long maxBytesLocalDisk) { + net.sf.ehcache.config.CacheConfiguration current = cache.getCacheConfiguration(); + // Build a fresh CacheConfiguration so that onDiskPoolUsage is unset and setMaxBytesLocalDisk + // can be applied without triggering the "can't switch disk pool" guard in Ehcache 2. + net.sf.ehcache.config.CacheConfiguration newConfig = new net.sf.ehcache.config.CacheConfiguration(id, + (int) current.getMaxEntriesLocalHeap()); + newConfig.setTimeToIdleSeconds(current.getTimeToIdleSeconds()); + newConfig.setTimeToLiveSeconds(current.getTimeToLiveSeconds()); + newConfig.setEternal(current.isEternal()); + newConfig.setMemoryStoreEvictionPolicy(current.getMemoryStoreEvictionPolicy().toString()); + newConfig.setMaxBytesLocalDisk(maxBytesLocalDisk); + rebuildCacheWith(newConfig); + } + + /** + * Removes the existing cache from the {@link CacheManager} and registers a new one built from {@code newConfig}. + * Subclasses may override this method to apply additional decorators (e.g. + * {@link net.sf.ehcache.constructs.blocking.BlockingCache}). + * + * @param newConfig + * the configuration to use for the replacement cache + */ + protected void rebuildCacheWith(net.sf.ehcache.config.CacheConfiguration newConfig) { + CACHE_MANAGER.removeCache(id); + CACHE_MANAGER.addCache(new net.sf.ehcache.Cache(newConfig)); + this.cache = CACHE_MANAGER.getEhcache(id); + } + /** * Sets the eviction policy. An invalid argument will set it to null. * diff --git a/src/main/java/org/mybatis/caches/ehcache/EhBlockingCache.java b/src/main/java/org/mybatis/caches/ehcache/EhBlockingCache.java index 70f2b90..7c6bbbe 100644 --- a/src/main/java/org/mybatis/caches/ehcache/EhBlockingCache.java +++ b/src/main/java/org/mybatis/caches/ehcache/EhBlockingCache.java @@ -20,7 +20,11 @@ import net.sf.ehcache.constructs.blocking.BlockingCache; /** - * The Class EhBlockingCache. + * Cache implementation that wraps Ehcache 2 with {@link BlockingCache} semantics. + *+ * {@link BlockingCache} acquires a per-key lock when a cache miss occurs so that only one thread computes the missing + * value while others block. This prevents cache-stampede on a cold or expired entry. + *
* * @author Iwao AVE! */ @@ -51,4 +55,20 @@ public Object removeObject(Object key) { return null; } + /** + * {@inheritDoc} + *+ * Re-wraps the rebuilt cache in a {@link BlockingCache} after replacing it. + *
+ */ + @Override + protected void rebuildCacheWith(net.sf.ehcache.config.CacheConfiguration newConfig) { + CACHE_MANAGER.removeCache(id); + CACHE_MANAGER.addCache(new net.sf.ehcache.Cache(newConfig)); + Ehcache ehcache = CACHE_MANAGER.getEhcache(id); + BlockingCache blockingCache = new BlockingCache(ehcache); + CACHE_MANAGER.replaceCacheWithDecoratedCache(ehcache, blockingCache); + this.cache = CACHE_MANAGER.getEhcache(id); + } + } diff --git a/src/test/java/org/mybatis/caches/ehcache/EhBlockingCacheTest.java b/src/test/java/org/mybatis/caches/ehcache/EhBlockingCacheTest.java index b6ef96d..4d69d04 100644 --- a/src/test/java/org/mybatis/caches/ehcache/EhBlockingCacheTest.java +++ b/src/test/java/org/mybatis/caches/ehcache/EhBlockingCacheTest.java @@ -109,6 +109,43 @@ void shouldTestEvictionPolicy() throws Exception { this.resetCache(); } + @Test + void shouldSetMaxBytesLocalDisk() { + // Use a distinct cache ID to avoid interfering with the shared EHBLOCKINGCACHE used by other tests. + // setMaxBytesLocalDisk triggers a cache rebuild because Ehcache 2 does not allow enabling + // the byte-based disk pool on an already-running cache instance. The rebuild re-applies the + // BlockingCache decorator so locking semantics are preserved. + AbstractEhcacheCache diskCache = new EhBlockingCache("EHBLOCKINGCACHE_DISK_TEST"); + try { + diskCache.setMaxBytesLocalDisk(10 * 1024 * 1024L); // 10 MB + assertEquals(10 * 1024 * 1024L, diskCache.cache.getCacheConfiguration().getMaxBytesLocalDisk()); + assertEquals(0, diskCache.cache.getCacheConfiguration().getMaxEntriesLocalDisk()); + diskCache.putObject("key", "value"); + assertEquals("value", diskCache.getObject("key")); + } finally { + AbstractEhcacheCache.CACHE_MANAGER.removeCache("EHBLOCKINGCACHE_DISK_TEST"); + } + } + + @Test + void shouldSupportDiskOverflow() { + // Use a distinct cache ID to avoid interfering with the shared EHBLOCKINGCACHE used by other tests. + AbstractEhcacheCache diskCache = new EhBlockingCache("EHBLOCKINGCACHE_DISK_OVERFLOW_TEST"); + try { + diskCache.setMaxEntriesLocalHeap(1); // limit heap to 1 entry so others overflow to disk + diskCache.setMaxBytesLocalDisk(10 * 1024 * 1024L); // 10 MB — triggers rebuild with disk tier + diskCache.putObject("key1", "value1"); + diskCache.putObject("key2", "value2"); // key1 overflows to disk + diskCache.putObject("key3", "value3"); // key2 overflows to disk + // All entries must remain retrievable; heap-evicted entries should be found on disk. + assertEquals("value1", diskCache.getObject("key1")); + assertEquals("value2", diskCache.getObject("key2")); + assertEquals("value3", diskCache.getObject("key3")); + } finally { + AbstractEhcacheCache.CACHE_MANAGER.removeCache("EHBLOCKINGCACHE_DISK_OVERFLOW_TEST"); + } + } + @Test void shouldNotCreateCache() { assertThrows(IllegalArgumentException.class, () -> { diff --git a/src/test/java/org/mybatis/caches/ehcache/EhcacheTest.java b/src/test/java/org/mybatis/caches/ehcache/EhcacheTest.java index d7c6d08..d8c4d2f 100644 --- a/src/test/java/org/mybatis/caches/ehcache/EhcacheTest.java +++ b/src/test/java/org/mybatis/caches/ehcache/EhcacheTest.java @@ -109,6 +109,45 @@ void shouldTestEvictionPolicy() throws Exception { this.resetCache(); } + @Test + void shouldSetMaxBytesLocalDisk() { + // Use a distinct cache ID to avoid interfering with the shared EHCACHE used by other tests. + AbstractEhcacheCache diskCache = new EhcacheCache("EHCACHE_DISK_TEST"); + try { + // Setting maxBytesLocalDisk rebuilds the cache with the correct byte-based disk tier. + diskCache.setMaxBytesLocalDisk(10 * 1024 * 1024L); // 10 MB + assertEquals(10 * 1024 * 1024L, diskCache.cache.getCacheConfiguration().getMaxBytesLocalDisk()); + // maxEntriesLocalDisk must be 0 (the two settings are mutually exclusive in Ehcache 2). + assertEquals(0, diskCache.cache.getCacheConfiguration().getMaxEntriesLocalDisk()); + // Cache must still be functional after the rebuild. + diskCache.putObject("key", "value"); + assertEquals("value", diskCache.getObject("key")); + } finally { + AbstractEhcacheCache.CACHE_MANAGER.removeCache("EHCACHE_DISK_TEST"); + } + } + + @Test + void shouldSupportDiskOverflow() { + // Use a distinct cache ID to avoid interfering with the shared EHCACHE used by other tests. + // setMaxBytesLocalDisk triggers a cache rebuild because Ehcache 2 does not allow enabling + // the byte-based disk pool on an already-running cache instance. + AbstractEhcacheCache diskCache = new EhcacheCache("EHCACHE_DISK_OVERFLOW_TEST"); + try { + diskCache.setMaxEntriesLocalHeap(1); // limit heap to 1 entry so others overflow to disk + diskCache.setMaxBytesLocalDisk(10 * 1024 * 1024L); // 10 MB — triggers rebuild with disk tier + diskCache.putObject("key1", "value1"); + diskCache.putObject("key2", "value2"); // key1 overflows to disk + diskCache.putObject("key3", "value3"); // key2 overflows to disk + // All entries must remain retrievable; heap-evicted entries should be found on disk. + assertEquals("value1", diskCache.getObject("key1")); + assertEquals("value2", diskCache.getObject("key2")); + assertEquals("value3", diskCache.getObject("key3")); + } finally { + AbstractEhcacheCache.CACHE_MANAGER.removeCache("EHCACHE_DISK_OVERFLOW_TEST"); + } + } + @Test void shouldNotCreateCache() { assertThrows(IllegalArgumentException.class, () -> {