Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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.
* <p>
* 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.
* </p>
*
* @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.
*
Expand Down
22 changes: 21 additions & 1 deletion src/main/java/org/mybatis/caches/ehcache/EhBlockingCache.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@
import net.sf.ehcache.constructs.blocking.BlockingCache;

/**
* The Class EhBlockingCache.
* Cache implementation that wraps Ehcache 2 with {@link BlockingCache} semantics.
* <p>
* {@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.
* </p>
*
* @author Iwao AVE!
*/
Expand Down Expand Up @@ -51,4 +55,20 @@ public Object removeObject(Object key) {
return null;
}

/**
* {@inheritDoc}
* <p>
* Re-wraps the rebuilt cache in a {@link BlockingCache} after replacing it.
* </p>
*/
@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);
}

}
37 changes: 37 additions & 0 deletions src/test/java/org/mybatis/caches/ehcache/EhBlockingCacheTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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, () -> {
Expand Down
39 changes: 39 additions & 0 deletions src/test/java/org/mybatis/caches/ehcache/EhcacheTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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, () -> {
Expand Down