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 628c8351cb fix(sync): retain gateway cache during namespace snapshots 
(#5580) (#7416)
628c8351cb is described below

commit 628c8351cbde2a43116a8695184470bcca5d7053
Author: Jerry聊AI <[email protected]>
AuthorDate: Sun Oct 11 23:20:19 2026 +0800

    fix(sync): retain gateway cache during namespace snapshots (#5580) (#7416)
---
 .../base/cache/CommonPluginDataSubscriber.java     | 36 +++++++++++++
 .../base/cache/CommonPluginDataSubscriberTest.java | 61 ++++++++++++++++++++++
 .../shenyu/sync/data/api/PluginDataSubscriber.java | 33 ++++++++++++
 .../data/websocket/handler/PluginDataHandler.java  |  3 +-
 .../data/websocket/handler/RuleDataHandler.java    |  3 +-
 .../websocket/handler/SelectorDataHandler.java     |  3 +-
 .../handler/WebsocketDataHandlerTest.java          | 16 +++---
 7 files changed, 142 insertions(+), 13 deletions(-)

diff --git 
a/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/cache/CommonPluginDataSubscriber.java
 
b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/cache/CommonPluginDataSubscriber.java
index 181b95dad4..c72c39e6e0 100644
--- 
a/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/cache/CommonPluginDataSubscriber.java
+++ 
b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/cache/CommonPluginDataSubscriber.java
@@ -40,6 +40,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
+import java.util.Set;
 import java.util.stream.Collectors;
 
 /**
@@ -146,6 +147,41 @@ public class CommonPluginDataSubscriber implements 
PluginDataSubscriber {
         stale.forEach(this::unRuleSubscribe);
     }
 
+    @Override
+    public void applyPluginDataSnapshot(final String namespaceId, final 
List<PluginData> dataList) {
+        Set<String> pluginNames = 
dataList.stream().map(PluginData::getName).collect(Collectors.toSet());
+        List<PluginData> stale = 
BaseDataCache.getInstance().getPluginMap().values().stream()
+                .filter(data -> namespaceId.equals(data.getNamespaceId()))
+                .filter(data -> !pluginNames.contains(data.getName()))
+                .collect(Collectors.toList());
+        dataList.forEach(this::onSubscribe);
+        stale.forEach(this::unSubscribe);
+    }
+
+    @Override
+    public void applySelectorDataSnapshot(final String namespaceId, final 
List<SelectorData> dataList) {
+        Set<String> selectorIds = 
dataList.stream().map(SelectorData::getId).collect(Collectors.toSet());
+        List<SelectorData> stale = 
BaseDataCache.getInstance().getSelectorMap().values().stream()
+                .flatMap(List::stream)
+                .filter(data -> namespaceId.equals(data.getNamespaceId()))
+                .filter(data -> !selectorIds.contains(data.getId()))
+                .collect(Collectors.toList());
+        dataList.forEach(this::onSelectorSubscribe);
+        stale.forEach(this::unSelectorSubscribe);
+    }
+
+    @Override
+    public void applyRuleDataSnapshot(final String namespaceId, final 
List<RuleData> dataList) {
+        Set<String> ruleIds = 
dataList.stream().map(RuleData::getId).collect(Collectors.toSet());
+        List<RuleData> stale = 
BaseDataCache.getInstance().getRuleMap().values().stream()
+                .flatMap(List::stream)
+                .filter(data -> namespaceId.equals(data.getNamespaceId()))
+                .filter(data -> !ruleIds.contains(data.getId()))
+                .collect(Collectors.toList());
+        dataList.forEach(this::onRuleSubscribe);
+        stale.forEach(this::unRuleSubscribe);
+    }
+
     @Override
     public void refreshPluginDataAll() {
         BaseDataCache.getInstance().cleanPluginData();
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 cedf095d37..8487c594c4 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
@@ -109,6 +109,67 @@ public final class CommonPluginDataSubscriberTest {
         assertEquals(pluginData, 
baseDataCache.obtainPluginData(pluginData.getName()));
     }
 
+    @Test
+    void testPluginSnapshotKeepsExistingEntryUntilReplacementIsApplied() {
+        final PluginData oldPlugin = 
PluginData.builder().id("divide-id").name("divide").config("old")
+                .namespaceId("namespace-a").build();
+        final PluginData replacement = 
PluginData.builder().id("divide-id").name("divide").config("new")
+                .namespaceId("namespace-a").build();
+        final PluginData stale = 
PluginData.builder().id("removed-id").name("removed").namespaceId("namespace-a").build();
+        final PluginData otherNamespace = 
PluginData.builder().id("other-id").name("other").namespaceId("namespace-b").build();
+        baseDataCache.cachePluginData(oldPlugin);
+        baseDataCache.cachePluginData(stale);
+        baseDataCache.cachePluginData(otherNamespace);
+        doAnswer(invocation -> {
+            assertEquals(oldPlugin, baseDataCache.obtainPluginData("divide"));
+            return null;
+        }).when(handler).handlerPlugin(replacement);
+
+        commonPluginDataSubscriber.applyPluginDataSnapshot("namespace-a", 
List.of(replacement));
+
+        assertEquals(replacement, baseDataCache.obtainPluginData("divide"));
+        assertNull(baseDataCache.obtainPluginData("removed"));
+        assertEquals(otherNamespace, baseDataCache.obtainPluginData("other"));
+    }
+
+    @Test
+    void testSelectorSnapshotReplacesRowsAndRemovesOnlyStaleNamespaceRows() {
+        final SelectorData oldSelector = 
SelectorData.builder().id("selector-a").pluginName("divide")
+                .namespaceId("namespace-a").sort(1).build();
+        final SelectorData replacement = 
SelectorData.builder().id("selector-a").pluginName("divide")
+                .namespaceId("namespace-a").sort(2).build();
+        final SelectorData stale = 
SelectorData.builder().id("stale-selector").pluginName("divide")
+                .namespaceId("namespace-a").sort(3).build();
+        final SelectorData otherNamespace = 
SelectorData.builder().id("other-selector").pluginName("divide")
+                .namespaceId("namespace-b").sort(4).build();
+        baseDataCache.cacheSelectData(oldSelector);
+        baseDataCache.cacheSelectData(stale);
+        baseDataCache.cacheSelectData(otherNamespace);
+
+        commonPluginDataSubscriber.applySelectorDataSnapshot("namespace-a", 
List.of(replacement));
+
+        assertEquals(List.of(replacement, otherNamespace), 
baseDataCache.obtainSelectorData("divide"));
+    }
+
+    @Test
+    void testRuleSnapshotReplacesRowsAndRemovesOnlyStaleNamespaceRows() {
+        final RuleData oldRule = 
RuleData.builder().id("rule-a").selectorId("selector-a").pluginName("divide")
+                .namespaceId("namespace-a").sort(1).build();
+        final RuleData replacement = 
RuleData.builder().id("rule-a").selectorId("selector-a").pluginName("divide")
+                .namespaceId("namespace-a").sort(2).build();
+        final RuleData stale = 
RuleData.builder().id("stale-rule").selectorId("selector-a").pluginName("divide")
+                .namespaceId("namespace-a").sort(3).build();
+        final RuleData otherNamespace = 
RuleData.builder().id("other-rule").selectorId("selector-a").pluginName("divide")
+                .namespaceId("namespace-b").sort(4).build();
+        baseDataCache.cacheRuleData(oldRule);
+        baseDataCache.cacheRuleData(stale);
+        baseDataCache.cacheRuleData(otherNamespace);
+
+        commonPluginDataSubscriber.applyRuleDataSnapshot("namespace-a", 
List.of(replacement));
+
+        assertEquals(List.of(replacement, otherNamespace), 
baseDataCache.obtainRuleData("selector-a"));
+    }
+
     @Test
     void 
testInitialSyncPropagatesHandlerFailureWithoutChangingLegacyBehavior() {
         PluginDataHandler handler = mock(PluginDataHandler.class);
diff --git 
a/shenyu-sync-data-center/shenyu-sync-data-api/src/main/java/org/apache/shenyu/sync/data/api/PluginDataSubscriber.java
 
b/shenyu-sync-data-center/shenyu-sync-data-api/src/main/java/org/apache/shenyu/sync/data/api/PluginDataSubscriber.java
index 10026fdb3d..4bf3418607 100644
--- 
a/shenyu-sync-data-center/shenyu-sync-data-api/src/main/java/org/apache/shenyu/sync/data/api/PluginDataSubscriber.java
+++ 
b/shenyu-sync-data-center/shenyu-sync-data-api/src/main/java/org/apache/shenyu/sync/data/api/PluginDataSubscriber.java
@@ -177,4 +177,37 @@ public interface PluginDataSubscriber {
         throw new UnsupportedOperationException("Namespace-scoped rule 
snapshots are not supported");
     }
 
+    /**
+     * Apply an authoritative plugin snapshot while retaining cached rows 
present in the snapshot.
+     *
+     * @param namespaceId namespace to reconcile
+     * @param dataList complete plugin snapshot for the namespace
+     */
+    default void applyPluginDataSnapshot(final String namespaceId, final 
List<PluginData> dataList) {
+        refreshPluginDataNamespace(namespaceId);
+        dataList.forEach(this::onSubscribe);
+    }
+
+    /**
+     * Apply an authoritative selector snapshot while retaining cached rows 
present in the snapshot.
+     *
+     * @param namespaceId namespace to reconcile
+     * @param dataList complete selector snapshot for the namespace
+     */
+    default void applySelectorDataSnapshot(final String namespaceId, final 
List<SelectorData> dataList) {
+        refreshSelectorDataNamespace(namespaceId);
+        dataList.forEach(this::onSelectorSubscribe);
+    }
+
+    /**
+     * Apply an authoritative rule snapshot while retaining cached rows 
present in the snapshot.
+     *
+     * @param namespaceId namespace to reconcile
+     * @param dataList complete rule snapshot for the namespace
+     */
+    default void applyRuleDataSnapshot(final String namespaceId, final 
List<RuleData> dataList) {
+        refreshRuleDataNamespace(namespaceId);
+        dataList.forEach(this::onRuleSubscribe);
+    }
+
 }
diff --git 
a/shenyu-sync-data-center/shenyu-sync-data-websocket/src/main/java/org/apache/shenyu/plugin/sync/data/websocket/handler/PluginDataHandler.java
 
b/shenyu-sync-data-center/shenyu-sync-data-websocket/src/main/java/org/apache/shenyu/plugin/sync/data/websocket/handler/PluginDataHandler.java
index 055c95009d..0f0e38090c 100644
--- 
a/shenyu-sync-data-center/shenyu-sync-data-websocket/src/main/java/org/apache/shenyu/plugin/sync/data/websocket/handler/PluginDataHandler.java
+++ 
b/shenyu-sync-data-center/shenyu-sync-data-websocket/src/main/java/org/apache/shenyu/plugin/sync/data/websocket/handler/PluginDataHandler.java
@@ -50,8 +50,7 @@ public class PluginDataHandler extends 
AbstractDataHandler<PluginData> {
         if (dataList.stream().anyMatch(data -> 
!namespaceId.equals(data.getNamespaceId()))) {
             throw new IllegalArgumentException("Snapshot row namespace does 
not match the connection");
         }
-        pluginDataSubscriber.refreshPluginDataNamespace(namespaceId);
-        doUpdate(dataList);
+        pluginDataSubscriber.applyPluginDataSnapshot(namespaceId, dataList);
     }
 
     @Override
diff --git 
a/shenyu-sync-data-center/shenyu-sync-data-websocket/src/main/java/org/apache/shenyu/plugin/sync/data/websocket/handler/RuleDataHandler.java
 
b/shenyu-sync-data-center/shenyu-sync-data-websocket/src/main/java/org/apache/shenyu/plugin/sync/data/websocket/handler/RuleDataHandler.java
index 0fbdbed978..651b50dda3 100644
--- 
a/shenyu-sync-data-center/shenyu-sync-data-websocket/src/main/java/org/apache/shenyu/plugin/sync/data/websocket/handler/RuleDataHandler.java
+++ 
b/shenyu-sync-data-center/shenyu-sync-data-websocket/src/main/java/org/apache/shenyu/plugin/sync/data/websocket/handler/RuleDataHandler.java
@@ -49,8 +49,7 @@ public class RuleDataHandler extends 
AbstractDataHandler<RuleData> {
         if (dataList.stream().anyMatch(data -> 
!namespaceId.equals(data.getNamespaceId()))) {
             throw new IllegalArgumentException("Snapshot row namespace does 
not match the connection");
         }
-        pluginDataSubscriber.refreshRuleDataNamespace(namespaceId);
-        doUpdate(dataList);
+        pluginDataSubscriber.applyRuleDataSnapshot(namespaceId, dataList);
     }
 
     @Override
diff --git 
a/shenyu-sync-data-center/shenyu-sync-data-websocket/src/main/java/org/apache/shenyu/plugin/sync/data/websocket/handler/SelectorDataHandler.java
 
b/shenyu-sync-data-center/shenyu-sync-data-websocket/src/main/java/org/apache/shenyu/plugin/sync/data/websocket/handler/SelectorDataHandler.java
index 3b142c49c6..840d5708c9 100644
--- 
a/shenyu-sync-data-center/shenyu-sync-data-websocket/src/main/java/org/apache/shenyu/plugin/sync/data/websocket/handler/SelectorDataHandler.java
+++ 
b/shenyu-sync-data-center/shenyu-sync-data-websocket/src/main/java/org/apache/shenyu/plugin/sync/data/websocket/handler/SelectorDataHandler.java
@@ -50,8 +50,7 @@ public class SelectorDataHandler extends 
AbstractDataHandler<SelectorData> {
         if (dataList.stream().anyMatch(data -> 
!namespaceId.equals(data.getNamespaceId()))) {
             throw new IllegalArgumentException("Snapshot row namespace does 
not match the connection");
         }
-        pluginDataSubscriber.refreshSelectorDataNamespace(namespaceId);
-        doUpdate(dataList);
+        pluginDataSubscriber.applySelectorDataSnapshot(namespaceId, dataList);
     }
 
     @Override
diff --git 
a/shenyu-sync-data-center/shenyu-sync-data-websocket/src/test/java/org/apache/shenyu/plugin/sync/data/websocket/handler/WebsocketDataHandlerTest.java
 
b/shenyu-sync-data-center/shenyu-sync-data-websocket/src/test/java/org/apache/shenyu/plugin/sync/data/websocket/handler/WebsocketDataHandlerTest.java
index 1b69ddef9f..12ecff787e 100644
--- 
a/shenyu-sync-data-center/shenyu-sync-data-websocket/src/test/java/org/apache/shenyu/plugin/sync/data/websocket/handler/WebsocketDataHandlerTest.java
+++ 
b/shenyu-sync-data-center/shenyu-sync-data-websocket/src/test/java/org/apache/shenyu/plugin/sync/data/websocket/handler/WebsocketDataHandlerTest.java
@@ -41,6 +41,7 @@ import org.apache.shenyu.sync.data.api.PluginDataSubscriber;
 import org.apache.shenyu.sync.data.api.ProxySelectorDataSubscriber;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.springframework.context.ApplicationEventPublisher;
 import org.mockito.Mockito;
 
 public final class WebsocketDataHandlerTest {
@@ -156,7 +157,7 @@ public final class WebsocketDataHandlerTest {
     @Test
     public void testEmptySnapshotClearsOnlyItsGroup() {
         websocketDataHandler.snapshot(ConfigGroupEnum.RULE, "[]", 
"namespace-a", "namespace-a");
-        verify(pluginDataSubscriber).refreshRuleDataNamespace("namespace-a");
+        verify(pluginDataSubscriber).applyRuleDataSnapshot("namespace-a", 
Collections.emptyList());
         Mockito.verifyNoMoreInteractions(pluginDataSubscriber);
     }
 
@@ -177,15 +178,14 @@ public final class WebsocketDataHandlerTest {
     @Test
     public void testSnapshotReplacesStalePluginBeforeSubscribing() {
         websocketDataHandler.snapshot(ConfigGroupEnum.PLUGIN, getJson(), 
"namespace-a", "namespace-a");
-        org.mockito.InOrder order = Mockito.inOrder(pluginDataSubscriber);
-        
order.verify(pluginDataSubscriber).refreshPluginDataNamespace("namespace-a");
-        
order.verify(pluginDataSubscriber).onSubscribe(Mockito.any(PluginData.class));
+        
verify(pluginDataSubscriber).applyPluginDataSnapshot(Mockito.eq("namespace-a"), 
Mockito.anyList());
     }
 
     @Test
     public void testEmptySnapshotsRemoveStaleGatewayCache() {
         BaseDataCache cache = BaseDataCache.getInstance();
         PluginDataSubscriber subscriber = new 
CommonPluginDataSubscriber(Collections.emptyList(),
+                mock(ApplicationEventPublisher.class),
                 new ShenyuConfig.SelectorMatchCache(), new 
ShenyuConfig.RuleMatchCache());
         WebsocketDataHandler handler = new WebsocketDataHandler(subscriber, 
Collections.emptyList(), Collections.emptyList(),
                 Collections.emptyList(), Collections.emptyList(), 
Collections.emptyList());
@@ -229,6 +229,7 @@ public final class WebsocketDataHandlerTest {
     public void testEmptyNamespaceSnapshotPreservesOtherNamespaces() {
         BaseDataCache cache = BaseDataCache.getInstance();
         PluginDataSubscriber subscriber = new 
CommonPluginDataSubscriber(Collections.emptyList(),
+                mock(ApplicationEventPublisher.class),
                 new ShenyuConfig.SelectorMatchCache(), new 
ShenyuConfig.RuleMatchCache());
         WebsocketDataHandler handler = new WebsocketDataHandler(subscriber, 
Collections.emptyList(), Collections.emptyList(),
                 Collections.emptyList(), Collections.emptyList(), 
Collections.emptyList());
@@ -261,14 +262,15 @@ public final class WebsocketDataHandlerTest {
     public void 
testNonEmptySnapshotRemovesMissingRulesAndPreservesOtherNamespace() {
         BaseDataCache cache = BaseDataCache.getInstance();
         PluginDataSubscriber subscriber = new 
CommonPluginDataSubscriber(Collections.emptyList(),
+                mock(ApplicationEventPublisher.class),
                 new ShenyuConfig.SelectorMatchCache(), new 
ShenyuConfig.RuleMatchCache());
         WebsocketDataHandler handler = new WebsocketDataHandler(subscriber, 
Collections.emptyList(), Collections.emptyList(),
                 Collections.emptyList(), Collections.emptyList(), 
Collections.emptyList());
-        RuleData stale = 
RuleData.builder().id("stale-rule").selectorId("selector-a")
+        RuleData stale = 
RuleData.builder().id("stale-rule").selectorId("selector-a").pluginName("divide")
                 .namespaceId("namespace-a").sort(1).build();
-        RuleData replacement = 
RuleData.builder().id("replacement-rule").selectorId("selector-a")
+        RuleData replacement = 
RuleData.builder().id("replacement-rule").selectorId("selector-a").pluginName("divide")
                 .namespaceId("namespace-a").sort(2).build();
-        RuleData other = 
RuleData.builder().id("other-rule").selectorId("selector-b")
+        RuleData other = 
RuleData.builder().id("other-rule").selectorId("selector-b").pluginName("divide")
                 .namespaceId("namespace-b").sort(1).build();
         cache.cacheRuleData(stale);
         cache.cacheRuleData(other);

Reply via email to