From 09f23fb0e0361186e2dbc5608796b5cfd0edce37 Mon Sep 17 00:00:00 2001 From: konlue <2362586639@qq.com> Date: Mon, 19 Oct 2026 15:46:15 +0800 Subject: [PATCH] fix(base): resolve partial identities when removing metadata (#7790) --- .../plugin/base/cache/MetaDataCache.java | 31 +++++++++++- .../plugin/base/cache/MetaDataCacheTest.java | 50 +++++++++++++++++++ 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/cache/MetaDataCache.java b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/cache/MetaDataCache.java index f15df367850f..a6990e23d383 100644 --- a/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/cache/MetaDataCache.java +++ b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/cache/MetaDataCache.java @@ -98,13 +98,40 @@ public synchronized void cache(final MetaData data) { /** * Remove auth data. * + *

Deletion events may carry only half of the identity: node sync sends + * an id without a path, path sync sends a path without an id (#7790). + * The partial identity is resolved against the cached canonical record so + * both the id map and the derived path results are invalidated, and the + * removal stays idempotent instead of throwing.

+ * * @param data the data */ public synchronized void remove(final MetaData data) { - META_DATA_MAP.remove(data.getId()); + final String id = data.getId(); + final String path = data.getPath(); + final MetaData cached = Objects.nonNull(id) ? META_DATA_MAP.get(id) : findByPath(path); + if (Objects.nonNull(id)) { + META_DATA_MAP.remove(id); + } else if (Objects.nonNull(cached)) { + META_DATA_MAP.remove(cached.getId()); + } indexDirty = true; negativeCacheDirty = true; - clean(data.getPath()); + if (Objects.nonNull(path)) { + clean(path); + } else if (Objects.nonNull(cached) && Objects.nonNull(cached.getPath())) { + clean(cached.getPath()); + } + } + + private MetaData findByPath(final String path) { + if (Objects.isNull(path)) { + return null; + } + return META_DATA_MAP.values().stream() + .filter(metaData -> path.equals(metaData.getPath())) + .findFirst() + .orElse(null); } private void rebuildPathIndex() { diff --git a/shenyu-plugin/shenyu-plugin-base/src/test/java/org/apache/shenyu/plugin/base/cache/MetaDataCacheTest.java b/shenyu-plugin/shenyu-plugin-base/src/test/java/org/apache/shenyu/plugin/base/cache/MetaDataCacheTest.java index a7e6b43690aa..4088c173e3e6 100644 --- a/shenyu-plugin/shenyu-plugin-base/src/test/java/org/apache/shenyu/plugin/base/cache/MetaDataCacheTest.java +++ b/shenyu-plugin/shenyu-plugin-base/src/test/java/org/apache/shenyu/plugin/base/cache/MetaDataCacheTest.java @@ -21,6 +21,8 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; class MetaDataCacheTest { @@ -45,6 +47,54 @@ void shouldPreferMostSpecificMatchingPattern() { assertSame(specific, cache.obtain("/api/users/42")); } + @Test + void shouldRemoveByIdOnlyRecordWithoutPath() { + MetaData full = createMetaData("id-1", "/rpc/dubbo/save"); + cache.cache(full); + assertSame(full, cache.obtain("/rpc/dubbo/save")); + + MetaData idOnly = new MetaData(); + idOnly.setId("id-1"); + + assertDoesNotThrow(() -> cache.remove(idOnly)); + assertNull(cache.obtain("/rpc/dubbo/save")); + } + + @Test + void shouldRemoveByPathOnlyRecordWithoutId() { + MetaData full = createMetaData("id-2", "/rpc/sofa/find"); + cache.cache(full); + assertSame(full, cache.obtain("/rpc/sofa/find")); + + MetaData pathOnly = new MetaData(); + pathOnly.setPath("/rpc/sofa/find"); + + assertDoesNotThrow(() -> cache.remove(pathOnly)); + assertNull(cache.obtain("/rpc/sofa/find")); + } + + @Test + void shouldKeepRemovingFullRecord() { + MetaData full = createMetaData("id-3", "/rpc/http/get"); + cache.cache(full); + + cache.remove(createMetaData("id-3", "/rpc/http/get")); + + assertNull(cache.obtain("/rpc/http/get")); + } + + @Test + void shouldBeIdempotentForPartialRecords() { + MetaData idOnly = new MetaData(); + idOnly.setId("missing-id"); + MetaData pathOnly = new MetaData(); + pathOnly.setPath("/rpc/never/cached"); + + assertDoesNotThrow(() -> cache.remove(idOnly)); + assertDoesNotThrow(() -> cache.remove(pathOnly)); + assertDoesNotThrow(() -> cache.remove(idOnly)); + } + private MetaData createMetaData(final String id, final String path) { MetaData metaData = new MetaData(); metaData.setId(id);