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 e0b5a62b24 fix: avoid concurrent modification in base data cache 
(#7019)
e0b5a62b24 is described below

commit e0b5a62b241f5af5673152fd5ef3fda4550497cd
Author: TheoLi0905 <[email protected]>
AuthorDate: Wed Sep 2 12:22:16 2026 +0800

    fix: avoid concurrent modification in base data cache (#7019)
    
    Co-authored-by: aias00 <[email protected]>
---
 .../shenyu/plugin/base/cache/BaseDataCache.java    | 63 +++++++++++-----------
 .../plugin/base/cache/BaseDataCacheTest.java       | 53 ++++++++++++++++--
 .../base/cache/CommonPluginDataSubscriberTest.java |  8 +--
 .../web/controller/LocalPluginControllerTest.java  |  4 +-
 4 files changed, 85 insertions(+), 43 deletions(-)

diff --git 
a/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/cache/BaseDataCache.java
 
b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/cache/BaseDataCache.java
index 89cabc4e90..216bd4dba8 100644
--- 
a/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/cache/BaseDataCache.java
+++ 
b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/cache/BaseDataCache.java
@@ -17,14 +17,15 @@
 
 package org.apache.shenyu.plugin.base.cache;
 
-import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import org.apache.shenyu.common.dto.PluginData;
 import org.apache.shenyu.common.dto.RuleData;
 import org.apache.shenyu.common.dto.SelectorData;
 
+import java.util.ArrayList;
 import java.util.Comparator;
 import java.util.List;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.concurrent.ConcurrentMap;
 import java.util.stream.Collectors;
@@ -132,10 +133,12 @@ public final class BaseDataCache {
      */
     public void removeSelectData(final SelectorData selectorData) {
         Optional.ofNullable(selectorData).ifPresent(data -> {
-            final List<SelectorData> selectorDataList = 
SELECTOR_MAP.get(data.getPluginName());
-            synchronized (SELECTOR_MAP) {
-                Optional.ofNullable(selectorDataList).ifPresent(list -> 
list.removeIf(e -> e.getId().equals(data.getId())));
-            }
+            SELECTOR_MAP.computeIfPresent(data.getPluginName(), (key, value) 
-> {
+                final List<SelectorData> result = value.stream()
+                        .filter(selector -> !Objects.equals(selector.getId(), 
data.getId()))
+                        .collect(Collectors.toList());
+                return result.isEmpty() ? null : List.copyOf(result);
+            });
         });
     }
     
@@ -168,7 +171,7 @@ public final class BaseDataCache {
      * Obtain selector data list list.
      *
      * @param pluginName the plugin name
-     * @return the list
+     * @return the immutable snapshot, or {@code null} if no selector data 
exists
      */
     public List<SelectorData> obtainSelectorData(final String pluginName) {
         return SELECTOR_MAP.get(pluginName);
@@ -190,10 +193,12 @@ public final class BaseDataCache {
      */
     public void removeRuleData(final RuleData ruleData) {
         Optional.ofNullable(ruleData).ifPresent(data -> {
-            final List<RuleData> ruleDataList = 
RULE_MAP.get(data.getSelectorId());
-            synchronized (RULE_MAP) {
-                Optional.ofNullable(ruleDataList).ifPresent(list -> 
list.removeIf(rule -> rule.getId().equals(data.getId())));
-            }
+            RULE_MAP.computeIfPresent(data.getSelectorId(), (key, value) -> {
+                final List<RuleData> result = value.stream()
+                        .filter(rule -> !Objects.equals(rule.getId(), 
data.getId()))
+                        .collect(Collectors.toList());
+                return result.isEmpty() ? null : List.copyOf(result);
+            });
         });
     }
     
@@ -226,7 +231,7 @@ public final class BaseDataCache {
      * Obtain rule data list list.
      *
      * @param selectorId the selector id
-     * @return the list
+     * @return the immutable snapshot, or {@code null} if no rule data exists
      */
     public List<RuleData> obtainRuleData(final String selectorId) {
         return RULE_MAP.get(selectorId);
@@ -267,17 +272,13 @@ public final class BaseDataCache {
      */
     private void ruleAccept(final RuleData data) {
         String selectorId = data.getSelectorId();
-        synchronized (RULE_MAP) {
-            if (RULE_MAP.containsKey(selectorId)) {
-                List<RuleData> existList = RULE_MAP.get(selectorId);
-                final List<RuleData> resultList = existList.stream().filter(r 
-> !r.getId().equals(data.getId())).collect(Collectors.toList());
-                resultList.add(data);
-                final List<RuleData> collect = 
resultList.stream().sorted(Comparator.comparing(RuleData::getSort)).collect(Collectors.toList());
-                RULE_MAP.put(selectorId, collect);
-            } else {
-                RULE_MAP.put(selectorId, Lists.newArrayList(data));
-            }
-        }
+        RULE_MAP.compute(selectorId, (key, value) -> {
+            final List<RuleData> result = Objects.isNull(value) ? new 
ArrayList<>() : new ArrayList<>(value);
+            result.removeIf(rule -> Objects.equals(rule.getId(), 
data.getId()));
+            result.add(data);
+            result.sort(Comparator.comparing(RuleData::getSort));
+            return List.copyOf(result);
+        });
     }
 
     /**
@@ -287,16 +288,12 @@ public final class BaseDataCache {
      */
     private void selectorAccept(final SelectorData data) {
         String key = data.getPluginName();
-        synchronized (SELECTOR_MAP) {
-            if (SELECTOR_MAP.containsKey(key)) {
-                List<SelectorData> existList = SELECTOR_MAP.get(key);
-                final List<SelectorData> resultList = 
existList.stream().filter(r -> 
!r.getId().equals(data.getId())).collect(Collectors.toList());
-                resultList.add(data);
-                final List<SelectorData> collect = 
resultList.stream().sorted(Comparator.comparing(SelectorData::getSort)).collect(Collectors.toList());
-                SELECTOR_MAP.put(key, collect);
-            } else {
-                SELECTOR_MAP.put(key, Lists.newArrayList(data));
-            }
-        }
+        SELECTOR_MAP.compute(key, (pluginName, value) -> {
+            final List<SelectorData> result = Objects.isNull(value) ? new 
ArrayList<>() : new ArrayList<>(value);
+            result.removeIf(selector -> Objects.equals(selector.getId(), 
data.getId()));
+            result.add(data);
+            result.sort(Comparator.comparing(SelectorData::getSort));
+            return List.copyOf(result);
+        });
     }
 }
diff --git 
a/shenyu-plugin/shenyu-plugin-base/src/test/java/org/apache/shenyu/plugin/base/cache/BaseDataCacheTest.java
 
b/shenyu-plugin/shenyu-plugin-base/src/test/java/org/apache/shenyu/plugin/base/cache/BaseDataCacheTest.java
index 5f9ed46481..e2c0c5fc5a 100644
--- 
a/shenyu-plugin/shenyu-plugin-base/src/test/java/org/apache/shenyu/plugin/base/cache/BaseDataCacheTest.java
+++ 
b/shenyu-plugin/shenyu-plugin-base/src/test/java/org/apache/shenyu/plugin/base/cache/BaseDataCacheTest.java
@@ -24,12 +24,15 @@ import org.apache.shenyu.common.dto.SelectorData;
 import org.junit.jupiter.api.Test;
 
 import java.lang.reflect.Field;
+import java.util.Iterator;
 import java.util.List;
 import java.util.concurrent.ConcurrentHashMap;
 
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 
 /**
  * Test cases for BaseDataCache.
@@ -142,7 +145,7 @@ public final class BaseDataCacheTest {
         selectorMap.put(mockPluginName1, Lists.newArrayList(selectorData));
 
         BaseDataCache.getInstance().removeSelectData(selectorData);
-        assertEquals(Lists.newArrayList(), selectorMap.get(mockPluginName1));
+        assertNull(selectorMap.get(mockPluginName1));
     }
 
     @Test
@@ -167,7 +170,7 @@ public final class BaseDataCacheTest {
         selectorMap.put(mockPluginName2, 
Lists.newArrayList(secondCachedSelectorData));
 
         
BaseDataCache.getInstance().cleanSelectorDataSelf(Lists.newArrayList(firstCachedSelectorData));
-        assertEquals(Lists.newArrayList(), selectorMap.get(mockPluginName1));
+        assertNull(selectorMap.get(mockPluginName1));
         assertEquals(Lists.newArrayList(secondCachedSelectorData), 
selectorMap.get(mockPluginName2));
     }
 
@@ -181,6 +184,27 @@ public final class BaseDataCacheTest {
         assertEquals(Lists.newArrayList(selectorData), selectorDataList);
     }
 
+    @Test
+    public void testSelectorDataSnapshotRemainsStableAfterDelete() {
+        BaseDataCache.getInstance().cleanSelectorData();
+        SelectorData firstSelectorData = 
SelectorData.builder().id("1").pluginName(mockPluginName1).sort(1).build();
+        SelectorData secondSelectorData = 
SelectorData.builder().id("2").pluginName(mockPluginName1).sort(2).build();
+        BaseDataCache.getInstance().cacheSelectData(firstSelectorData);
+        BaseDataCache.getInstance().cacheSelectData(secondSelectorData);
+
+        List<SelectorData> snapshot = 
BaseDataCache.getInstance().obtainSelectorData(mockPluginName1);
+        Iterator<SelectorData> iterator = snapshot.iterator();
+        assertEquals(firstSelectorData, iterator.next());
+
+        BaseDataCache.getInstance().removeSelectData(firstSelectorData);
+
+        assertDoesNotThrow(() -> iterator.forEachRemaining(selector -> 
assertNotNull(selector)));
+        assertEquals(Lists.newArrayList(firstSelectorData, 
secondSelectorData), snapshot);
+        assertEquals(Lists.newArrayList(secondSelectorData), 
BaseDataCache.getInstance().obtainSelectorData(mockPluginName1));
+        assertThrows(UnsupportedOperationException.class, () -> 
snapshot.add(firstSelectorData));
+        BaseDataCache.getInstance().cleanSelectorData();
+    }
+
     @Test
     public void testCacheRuleData() throws NoSuchFieldException, 
IllegalAccessException {
         RuleData firstCachedRuleData = 
RuleData.builder().id("1").selectorId(mockSelectorId1).sort(1).build();
@@ -200,7 +224,7 @@ public final class BaseDataCacheTest {
         ruleMap.put(mockSelectorId1, Lists.newArrayList(ruleData));
 
         BaseDataCache.getInstance().removeRuleData(ruleData);
-        assertEquals(Lists.newArrayList(), ruleMap.get(mockSelectorId1));
+        assertNull(ruleMap.get(mockSelectorId1));
     }
 
     @Test
@@ -225,7 +249,7 @@ public final class BaseDataCacheTest {
         ruleMap.put(mockSelectorId2, Lists.newArrayList(secondCachedRuleData));
 
         
BaseDataCache.getInstance().cleanRuleDataSelf(Lists.newArrayList(firstCachedRuleData));
-        assertEquals(Lists.newArrayList(), ruleMap.get(mockSelectorId1));
+        assertNull(ruleMap.get(mockSelectorId1));
         assertEquals(Lists.newArrayList(secondCachedRuleData), 
ruleMap.get(mockSelectorId2));
     }
 
@@ -239,6 +263,27 @@ public final class BaseDataCacheTest {
         assertEquals(Lists.newArrayList(ruleData), ruleDataList);
     }
 
+    @Test
+    public void testRuleDataSnapshotRemainsStableAfterDelete() {
+        BaseDataCache.getInstance().cleanRuleData();
+        RuleData firstRuleData = 
RuleData.builder().id("1").selectorId(mockSelectorId1).sort(1).build();
+        RuleData secondRuleData = 
RuleData.builder().id("2").selectorId(mockSelectorId1).sort(2).build();
+        BaseDataCache.getInstance().cacheRuleData(firstRuleData);
+        BaseDataCache.getInstance().cacheRuleData(secondRuleData);
+
+        List<RuleData> snapshot = 
BaseDataCache.getInstance().obtainRuleData(mockSelectorId1);
+        Iterator<RuleData> iterator = snapshot.iterator();
+        assertEquals(firstRuleData, iterator.next());
+
+        BaseDataCache.getInstance().removeRuleData(firstRuleData);
+
+        assertDoesNotThrow(() -> iterator.forEachRemaining(rule -> 
assertNotNull(rule)));
+        assertEquals(Lists.newArrayList(firstRuleData, secondRuleData), 
snapshot);
+        assertEquals(Lists.newArrayList(secondRuleData), 
BaseDataCache.getInstance().obtainRuleData(mockSelectorId1));
+        assertThrows(UnsupportedOperationException.class, () -> 
snapshot.add(firstRuleData));
+        BaseDataCache.getInstance().cleanRuleData();
+    }
+
     @SuppressWarnings("rawtypes")
     private ConcurrentHashMap getFieldByName(final String name) throws 
NoSuchFieldException, IllegalAccessException {
         BaseDataCache baseDataCache = BaseDataCache.getInstance();
diff --git 
a/shenyu-plugin/shenyu-plugin-base/src/test/java/org/apache/shenyu/plugin/base/cache/CommonPluginDataSubscriberTest.java
 
b/shenyu-plugin/shenyu-plugin-base/src/test/java/org/apache/shenyu/plugin/base/cache/CommonPluginDataSubscriberTest.java
index cd1d9cd1cd..3df6109763 100644
--- 
a/shenyu-plugin/shenyu-plugin-base/src/test/java/org/apache/shenyu/plugin/base/cache/CommonPluginDataSubscriberTest.java
+++ 
b/shenyu-plugin/shenyu-plugin-base/src/test/java/org/apache/shenyu/plugin/base/cache/CommonPluginDataSubscriberTest.java
@@ -165,7 +165,7 @@ public final class CommonPluginDataSubscriberTest {
 
             commonPluginDataSubscriber.unSelectorSubscribe(selectorData);
 
-            assertEquals(Lists.newArrayList(), 
baseDataCache.obtainSelectorData(selectorData.getPluginName()));
+            
assertNull(baseDataCache.obtainSelectorData(selectorData.getPluginName()));
             assertNull(matchDataCache.obtainSelectorData(mockPluginName1, 
path));
             assertNull(matchDataCache.obtainRuleData(mockPluginName1, path));
             assertNull(matchDataCache.obtainRuleData(mockPluginName1, 
emptyRulePath));
@@ -202,7 +202,7 @@ public final class CommonPluginDataSubscriberTest {
         
assertNotNull(baseDataCache.obtainSelectorData(secondCachedSelectorData.getPluginName()));
 
         
commonPluginDataSubscriber.refreshSelectorDataSelf(Lists.newArrayList(firstCachedSelectorData));
-        assertEquals(Lists.newArrayList(), 
baseDataCache.obtainSelectorData(firstCachedSelectorData.getPluginName()));
+        
assertNull(baseDataCache.obtainSelectorData(firstCachedSelectorData.getPluginName()));
         assertEquals(Lists.newArrayList(secondCachedSelectorData), 
baseDataCache.obtainSelectorData(secondCachedSelectorData.getPluginName()));
     }
 
@@ -224,7 +224,7 @@ public final class CommonPluginDataSubscriberTest {
         assertNotNull(baseDataCache.obtainRuleData(ruleData.getSelectorId()));
 
         commonPluginDataSubscriber.unRuleSubscribe(ruleData);
-        assertEquals(Lists.newArrayList(), 
baseDataCache.obtainRuleData(ruleData.getSelectorId()));
+        assertNull(baseDataCache.obtainRuleData(ruleData.getSelectorId()));
     }
 
     @Test
@@ -253,7 +253,7 @@ public final class CommonPluginDataSubscriberTest {
         
assertNotNull(baseDataCache.obtainRuleData(firstCachedRuleData.getSelectorId()));
 
         
commonPluginDataSubscriber.refreshRuleDataSelf(Lists.newArrayList(firstCachedRuleData));
-        assertEquals(Lists.newArrayList(), 
baseDataCache.obtainRuleData(firstCachedRuleData.getSelectorId()));
+        
assertNull(baseDataCache.obtainRuleData(firstCachedRuleData.getSelectorId()));
         assertEquals(Lists.newArrayList(secondCachedRuleData), 
baseDataCache.obtainRuleData(secondCachedRuleData.getSelectorId()));
     }
 
diff --git 
a/shenyu-web/src/test/java/org/apache/shenyu/web/controller/LocalPluginControllerTest.java
 
b/shenyu-web/src/test/java/org/apache/shenyu/web/controller/LocalPluginControllerTest.java
index 434eb87d8e..5b99aee226 100644
--- 
a/shenyu-web/src/test/java/org/apache/shenyu/web/controller/LocalPluginControllerTest.java
+++ 
b/shenyu-web/src/test/java/org/apache/shenyu/web/controller/LocalPluginControllerTest.java
@@ -238,7 +238,7 @@ public final class LocalPluginControllerTest {
                         .param("id", testSelectorId))
                 .andExpect(status().isOk())
                 .andReturn();
-        
assertThat(baseDataCache.obtainSelectorData(selectorPluginName)).isEmpty();
+        
assertThat(baseDataCache.obtainSelectorData(selectorPluginName)).isNull();
     }
 
     @Test
@@ -390,7 +390,7 @@ public final class LocalPluginControllerTest {
                 .andReturn();
 
         final List<RuleData> selectorId = 
baseDataCache.obtainRuleData(testSelectorId);
-        Assertions.assertTrue(selectorId.isEmpty());
+        Assertions.assertNull(selectorId);
     }
 
     @Test

Reply via email to