This is an automated email from the ASF dual-hosted git repository.
Aias00 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shenyu.git
The following commit(s) were added to refs/heads/master by this push:
new 6dc19834f2 fix(plugin): prevent expired memory cache entries from
accumulating (#7745)
6dc19834f2 is described below
commit 6dc19834f2135365e476bffdafa2c44bda5f44be
Author: lymerin <[email protected]>
AuthorDate: Mon Oct 19 09:29:10 2026 +0800
fix(plugin): prevent expired memory cache entries from accumulating (#7745)
---
.../shenyu-plugin-cache-memory/pom.xml | 4 ++
.../shenyu/plugin/cache/memory/MemoryCache.java | 55 +++++++++++--------
.../plugin/cache/memory/MemoryCacheTest.java | 62 ++++++++++++++++++----
3 files changed, 88 insertions(+), 33 deletions(-)
diff --git
a/shenyu-plugin/shenyu-plugin-cache/shenyu-plugin-cache-memory/pom.xml
b/shenyu-plugin/shenyu-plugin-cache/shenyu-plugin-cache-memory/pom.xml
index be179c1480..be1860cc74 100644
--- a/shenyu-plugin/shenyu-plugin-cache/shenyu-plugin-cache-memory/pom.xml
+++ b/shenyu-plugin/shenyu-plugin-cache/shenyu-plugin-cache-memory/pom.xml
@@ -27,6 +27,10 @@
<artifactId>shenyu-plugin-cache-memory</artifactId>
<dependencies>
+ <dependency>
+ <groupId>com.github.ben-manes.caffeine</groupId>
+ <artifactId>caffeine</artifactId>
+ </dependency>
<dependency>
<groupId>org.apache.shenyu</groupId>
<artifactId>shenyu-plugin-cache-spi</artifactId>
diff --git
a/shenyu-plugin/shenyu-plugin-cache/shenyu-plugin-cache-memory/src/main/java/org/apache/shenyu/plugin/cache/memory/MemoryCache.java
b/shenyu-plugin/shenyu-plugin-cache/shenyu-plugin-cache-memory/src/main/java/org/apache/shenyu/plugin/cache/memory/MemoryCache.java
index b51553431c..97e7073d86 100644
---
a/shenyu-plugin/shenyu-plugin-cache/shenyu-plugin-cache-memory/src/main/java/org/apache/shenyu/plugin/cache/memory/MemoryCache.java
+++
b/shenyu-plugin/shenyu-plugin-cache/shenyu-plugin-cache-memory/src/main/java/org/apache/shenyu/plugin/cache/memory/MemoryCache.java
@@ -17,15 +17,15 @@
package org.apache.shenyu.plugin.cache.memory;
-import com.google.common.cache.Cache;
-import com.google.common.cache.CacheBuilder;
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
+import com.github.benmanes.caffeine.cache.Expiry;
+import com.github.benmanes.caffeine.cache.Scheduler;
import org.apache.shenyu.plugin.cache.ICache;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
-import java.util.Map;
import java.util.Objects;
-import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
/**
@@ -33,10 +33,28 @@ import java.util.concurrent.TimeUnit;
*/
public final class MemoryCache implements ICache {
- private final Map<String, Cache<String, byte[]>> mainCache;
+ private final Cache<String, byte[]> mainCache;
public MemoryCache() {
- this.mainCache = new ConcurrentHashMap<>();
+ // Entries must be written through VarExpiration.put(...) to receive
their per-entry TTL.
+ this.mainCache = Caffeine.newBuilder()
+ .scheduler(Scheduler.systemScheduler())
+ .expireAfter(new Expiry<String, byte[]>() {
+ @Override
+ public long expireAfterCreate(final String key, final
byte[] value, final long currentTime) {
+ return 0;
+ }
+
+ @Override
+ public long expireAfterUpdate(final String key, final
byte[] value, final long currentTime, final long currentDuration) {
+ return 0;
+ }
+
+ @Override
+ public long expireAfterRead(final String key, final byte[]
value, final long currentTime, final long currentDuration) {
+ return currentDuration;
+ }
+ }).build();
}
/**
@@ -49,9 +67,10 @@ public final class MemoryCache implements ICache {
*/
@Override
public Mono<Boolean> cacheData(final String key, final byte[] bytes, final
long timeoutSeconds) {
- final Cache<String, byte[]> cache =
CacheBuilder.newBuilder().expireAfterWrite(timeoutSeconds,
TimeUnit.SECONDS).build();
- cache.put(key, bytes);
- this.mainCache.put(key, cache);
+ // Write the value and its TTL together, overriding the default expiry
callbacks.
+ this.mainCache.policy().expireVariably()
+ .orElseThrow(() -> new IllegalStateException("Variable
expiration is not configured for the memory cache"))
+ .put(key, bytes, timeoutSeconds, TimeUnit.SECONDS);
return Mono.fromCallable(() ->
Boolean.TRUE).subscribeOn(Schedulers.boundedElastic());
}
@@ -63,13 +82,7 @@ public final class MemoryCache implements ICache {
*/
@Override
public Mono<Boolean> isExist(final String key) {
- final Cache<String, byte[]> cache = this.mainCache.get(key);
- if (Objects.isNull(cache) || !cache.asMap().containsKey(key)) {
- // remove from main cache
- this.mainCache.remove(key);
- return Mono.fromCallable(() ->
Boolean.FALSE).subscribeOn(Schedulers.boundedElastic());
- }
- return Mono.fromCallable(() ->
Boolean.TRUE).subscribeOn(Schedulers.boundedElastic());
+ return Mono.fromCallable(() ->
Objects.nonNull(this.mainCache.getIfPresent(key))).subscribeOn(Schedulers.boundedElastic());
}
/**
@@ -80,12 +93,7 @@ public final class MemoryCache implements ICache {
*/
@Override
public Mono<byte[]> getData(final String key) {
- return isExist(key).mapNotNull(exist -> {
- if (exist) {
- return this.mainCache.get(key).asMap().get(key);
- }
- return null;
- });
+ return Mono.fromCallable(() ->
this.mainCache.getIfPresent(key)).subscribeOn(Schedulers.boundedElastic());
}
/**
@@ -93,6 +101,7 @@ public final class MemoryCache implements ICache {
*/
@Override
public void close() {
- this.mainCache.clear();
+ this.mainCache.invalidateAll();
+ this.mainCache.cleanUp();
}
}
diff --git
a/shenyu-plugin/shenyu-plugin-cache/shenyu-plugin-cache-memory/src/test/java/org/apache/shenyu/plugin/cache/memory/MemoryCacheTest.java
b/shenyu-plugin/shenyu-plugin-cache/shenyu-plugin-cache-memory/src/test/java/org/apache/shenyu/plugin/cache/memory/MemoryCacheTest.java
index ee3cd39a4c..132f485c4a 100644
---
a/shenyu-plugin/shenyu-plugin-cache/shenyu-plugin-cache-memory/src/test/java/org/apache/shenyu/plugin/cache/memory/MemoryCacheTest.java
+++
b/shenyu-plugin/shenyu-plugin-cache/shenyu-plugin-cache-memory/src/test/java/org/apache/shenyu/plugin/cache/memory/MemoryCacheTest.java
@@ -17,29 +17,71 @@
package org.apache.shenyu.plugin.cache.memory;
-import org.junit.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.junit.jupiter.MockitoExtension;
+import com.github.benmanes.caffeine.cache.Cache;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.test.util.ReflectionTestUtils;
import java.nio.charset.StandardCharsets;
+import java.util.concurrent.TimeUnit;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.awaitility.Awaitility.await;
/**
* MemoryCacheTest.
*/
-@ExtendWith(MockitoExtension.class)
public class MemoryCacheTest {
+ private final MemoryCache memoryCache = new MemoryCache();
+
+ @AfterEach
+ public void tearDown() {
+ memoryCache.close();
+ }
+
@Test
public void testMemoryCache() {
- final MemoryCache memoryCache = new MemoryCache();
final String key = "data";
- memoryCache.isExist(key).subscribe(v -> assertEquals(Boolean.FALSE,
v));
- memoryCache.cacheData(key, "data".getBytes(StandardCharsets.UTF_8), 10)
- .subscribe(v -> assertEquals(Boolean.TRUE, v));
- memoryCache.isExist(key).subscribe(v -> assertEquals(Boolean.TRUE, v));
- memoryCache.getData(key).subscribe(v -> assertEquals("data", new
String(v, StandardCharsets.UTF_8)));
+ final byte[] bytes = "data".getBytes(StandardCharsets.UTF_8);
+ assertEquals(Boolean.FALSE, memoryCache.isExist(key).block());
+ assertEquals(Boolean.TRUE, memoryCache.cacheData(key, bytes,
10).block());
+ assertEquals(Boolean.TRUE, memoryCache.isExist(key).block());
+ assertArrayEquals(bytes, memoryCache.getData(key).block());
+ memoryCache.close();
+ assertEquals(Boolean.FALSE, memoryCache.isExist(key).block());
+ assertNull(memoryCache.getData(key).block());
+ }
+
+ @Test
+ public void testExpiredEntriesAreRemovedWithoutReads() {
+ for (int i = 0; i < 100; i++) {
+ memoryCache.cacheData("key-" + i, new byte[]{1}, 1).block();
+ }
+ Cache<?, ?> cache = (Cache<?, ?>)
ReflectionTestUtils.getField(memoryCache, "mainCache");
+ // estimatedSize observes retained entries without triggering reads or
maintenance.
+ await().atMost(10, TimeUnit.SECONDS).untilAsserted(() ->
assertEquals(0L, cache.estimatedSize()));
+ }
+
+ @Test
+ public void testPerEntryExpirationAndReplacement() {
+ final byte[] bytes = "data".getBytes(StandardCharsets.UTF_8);
+ memoryCache.cacheData("short", bytes, 1).block();
+ memoryCache.cacheData("long", bytes, 60).block();
+ memoryCache.cacheData("extended", bytes, 1).block();
+ memoryCache.cacheData("extended", bytes, 60).block();
+ memoryCache.cacheData("shortened", bytes, 60).block();
+ memoryCache.cacheData("shortened", bytes, 1).block();
+
+ // Polling the short entry must not extend its write TTL.
+ await().pollInterval(50, TimeUnit.MILLISECONDS).atMost(10,
TimeUnit.SECONDS).untilAsserted(() -> {
+ assertNull(memoryCache.getData("short").block());
+ assertNull(memoryCache.getData("shortened").block());
+ });
+ assertArrayEquals(bytes, memoryCache.getData("long").block());
+ assertArrayEquals(bytes, memoryCache.getData("extended").block());
}
}