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 3d9678ffef perf(admin): batch discovery synchronization queries (#7268)
3d9678ffef is described below

commit 3d9678ffef656f0090c98fdce80d9ef001429d36
Author: Liming Deng <[email protected]>
AuthorDate: Tue Oct 20 14:30:31 2026 +0800

    perf(admin): batch discovery synchronization queries (#7268)
    
    * perf(admin): batch discovery synchronization queries
    
    * fix(admin): tolerate and report invalid discovery batch bindings
    
    * fix(admin): guard empty discovery relation batch queries
    
    * fix(admin): preserve rollback when batch upstream sync fails
---
 .../shenyu/admin/mapper/DiscoveryRelMapper.java    |   8 ++
 .../service/impl/DiscoveryUpstreamServiceImpl.java |  93 ++++++++-----
 .../resources/mappers/discovery-rel-sqlmap.xml     |  15 ++
 .../admin/mapper/DiscoveryBatchQueryTest.java      |  78 +++++++++++
 .../service/DiscoveryUpstreamServiceTest.java      | 153 +++++++++++++++++++--
 5 files changed, 301 insertions(+), 46 deletions(-)

diff --git 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/mapper/DiscoveryRelMapper.java
 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/mapper/DiscoveryRelMapper.java
index 6b764382d2..0c4dd0b33d 100644
--- 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/mapper/DiscoveryRelMapper.java
+++ 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/mapper/DiscoveryRelMapper.java
@@ -44,6 +44,14 @@ public interface DiscoveryRelMapper {
      */
     List<DiscoveryRelDO> selectAll();
 
+    /**
+     * Select rows for a bounded batch of discovery handlers.
+     *
+     * @param handlerIds handler ids
+     * @return matching rows
+     */
+    List<DiscoveryRelDO> selectByDiscoveryHandlerIds(@Param("handlerIds") 
List<String> handlerIds);
+
     /**
      * Select related rows for one page in a single query.
      *
diff --git 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/DiscoveryUpstreamServiceImpl.java
 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/DiscoveryUpstreamServiceImpl.java
index 515a6b7e63..0004cb43c0 100644
--- 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/DiscoveryUpstreamServiceImpl.java
+++ 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/DiscoveryUpstreamServiceImpl.java
@@ -54,6 +54,7 @@ import 
org.springframework.transaction.support.TransactionSynchronization;
 import 
org.springframework.transaction.support.TransactionSynchronizationManager;
 import org.springframework.util.StringUtils;
 
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.LinkedHashSet;
@@ -62,12 +63,16 @@ import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
 import java.util.stream.Collectors;
+import java.util.function.Function;
 
 @Service
 public class DiscoveryUpstreamServiceImpl implements DiscoveryUpstreamService {
 
     private static final Logger LOG = 
LoggerFactory.getLogger(DiscoveryUpstreamServiceImpl.class);
 
+    // Keep IN lists below Oracle's 1000-expression limit and bound query 
payloads on all databases.
+    private static final int BATCH_SIZE = 500;
+
     private final DiscoveryUpstreamMapper discoveryUpstreamMapper;
 
     private final DiscoveryHandlerMapper discoveryHandlerMapper;
@@ -145,6 +150,11 @@ public class DiscoveryUpstreamServiceImpl implements 
DiscoveryUpstreamService {
             discoveryUpstreamDO.setDiscoveryHandlerId(discoveryHandlerId);
             discoveryUpstreamMapper.insert(discoveryUpstreamDO);
         }
+        fetchAfterCommit(discoveryHandlerId);
+        return 0;
+    }
+
+    private void fetchAfterCommit(final String discoveryHandlerId) {
         if (TransactionSynchronizationManager.isSynchronizationActive()) {
             TransactionSynchronizationManager.registerSynchronization(new 
TransactionSynchronization() {
                 @Override
@@ -155,7 +165,6 @@ public class DiscoveryUpstreamServiceImpl implements 
DiscoveryUpstreamService {
         } else {
             this.fetchAll(discoveryHandlerId);
         }
-        return 0;
     }
 
     @Override
@@ -197,41 +206,59 @@ public class DiscoveryUpstreamServiceImpl implements 
DiscoveryUpstreamService {
         return 
buildSyncData(discoveryHandlerMapper.selectAllByNamespaceId(namespaceId));
     }
 
-    private List<DiscoverySyncData> buildSyncData(final 
List<DiscoveryHandlerDO> discoveryHandlerDOS) {
-        return discoveryHandlerDOS.stream().map(d -> {
-            DiscoveryRelDO discoveryRelDO = 
discoveryRelMapper.selectByDiscoveryHandlerId(d.getId());
-            if (Objects.isNull(discoveryRelDO)) {
-                LOG.warn("Skipping discovery handler {} without a relation", 
d.getId());
-                return null;
-            }
-            DiscoverySyncData discoverySyncData = new DiscoverySyncData();
-            discoverySyncData.setPluginName(discoveryRelDO.getPluginName());
-            if (StringUtils.hasLength(discoveryRelDO.getSelectorId())) {
-                String selectorId = discoveryRelDO.getSelectorId();
-                discoverySyncData.setSelectorId(selectorId);
-                SelectorDO selectorDO = selectorMapper.selectById(selectorId);
-                if (Objects.isNull(selectorDO)) {
-                    LOG.warn("Skipping discovery handler {} with missing 
selector {}", d.getId(), selectorId);
-                    return null;
+    private List<DiscoverySyncData> buildSyncData(final 
List<DiscoveryHandlerDO> handlers) {
+        List<DiscoverySyncData> result = new ArrayList<>();
+        for (List<DiscoveryHandlerDO> batch : Lists.partition(handlers, 
BATCH_SIZE)) {
+            List<String> handlerIds = 
batch.stream().map(DiscoveryHandlerDO::getId).collect(Collectors.toList());
+            List<DiscoveryRelDO> relations = 
discoveryRelMapper.selectByDiscoveryHandlerIds(handlerIds);
+            Set<String> selectorIds = 
relations.stream().map(DiscoveryRelDO::getSelectorId).filter(StringUtils::hasLength).collect(Collectors.toSet());
+            List<String> proxyIds = relations.stream().filter(rel -> 
!StringUtils.hasLength(rel.getSelectorId()))
+                    
.map(DiscoveryRelDO::getProxySelectorId).filter(StringUtils::hasLength).distinct().collect(Collectors.toList());
+            Map<String, SelectorDO> selectors = selectorIds.isEmpty() ? 
Collections.emptyMap()
+                    : 
selectorMapper.selectByIdSet(selectorIds).stream().collect(Collectors.toMap(SelectorDO::getId,
 Function.identity()));
+            Map<String, ProxySelectorDO> proxies = proxyIds.isEmpty() ? 
Collections.emptyMap()
+                    : 
proxySelectorMapper.selectByIds(proxyIds).stream().collect(Collectors.toMap(ProxySelectorDO::getId,
 Function.identity()));
+            Map<String, List<DiscoveryUpstreamData>> upstreams = 
discoveryUpstreamMapper.selectByDiscoveryHandlerIds(handlerIds).stream()
+                    
.collect(Collectors.groupingBy(DiscoveryUpstreamDO::getDiscoveryHandlerId,
+                            
Collectors.mapping(DiscoveryTransfer.INSTANCE::mapToData, 
Collectors.toList())));
+            Map<String, DiscoveryRelDO> relationByHandler = relations.stream()
+                    
.collect(Collectors.toMap(DiscoveryRelDO::getDiscoveryHandlerId, 
Function.identity(), (existing, duplicate) -> {
+                        LOG.warn("Duplicate discovery relations for handler 
{}, retaining relation {} and ignoring {}",
+                                existing.getDiscoveryHandlerId(), 
existing.getId(), duplicate.getId());
+                        return existing;
+                    }));
+            for (DiscoveryHandlerDO handler : batch) {
+                DiscoveryRelDO relation = 
relationByHandler.get(handler.getId());
+                if (Objects.isNull(relation)) {
+                    LOG.warn("Skipping discovery handler {}: relation is 
missing", handler.getId());
+                    continue;
                 }
-                
discoverySyncData.setSelectorName(selectorDO.getSelectorName());
-                discoverySyncData.setNamespaceId(selectorDO.getNamespaceId());
-            } else {
-                String proxySelectorId = discoveryRelDO.getProxySelectorId();
-                discoverySyncData.setSelectorId(proxySelectorId);
-                ProxySelectorDO proxySelectorDO = 
proxySelectorMapper.selectById(proxySelectorId);
-                if (Objects.isNull(proxySelectorDO)) {
-                    LOG.warn("Skipping discovery handler {} with missing proxy 
selector {}", d.getId(), proxySelectorId);
-                    return null;
+                DiscoverySyncData data = new DiscoverySyncData();
+                data.setPluginName(relation.getPluginName());
+                if (StringUtils.hasLength(relation.getSelectorId())) {
+                    SelectorDO selector = 
selectors.get(relation.getSelectorId());
+                    if (Objects.isNull(selector)) {
+                        LOG.warn("Skipping discovery handler {}: selector {} 
is missing", handler.getId(), relation.getSelectorId());
+                        continue;
+                    }
+                    data.setSelectorId(selector.getId());
+                    data.setSelectorName(selector.getSelectorName());
+                    data.setNamespaceId(selector.getNamespaceId());
+                } else {
+                    ProxySelectorDO proxy = 
proxies.get(relation.getProxySelectorId());
+                    if (Objects.isNull(proxy)) {
+                        LOG.warn("Skipping discovery handler {}: proxy 
selector {} is missing", handler.getId(), relation.getProxySelectorId());
+                        continue;
+                    }
+                    data.setSelectorId(proxy.getId());
+                    data.setSelectorName(proxy.getName());
+                    data.setNamespaceId(proxy.getNamespaceId());
                 }
-                discoverySyncData.setSelectorName(proxySelectorDO.getName());
-                
discoverySyncData.setNamespaceId(proxySelectorDO.getNamespaceId());
+                
data.setUpstreamDataList(upstreams.getOrDefault(handler.getId(), 
Collections.emptyList()));
+                result.add(data);
             }
-            List<DiscoveryUpstreamData> discoveryUpstreamDataList = 
discoveryUpstreamMapper.selectByDiscoveryHandlerId(d.getId()).stream()
-                    
.map(DiscoveryTransfer.INSTANCE::mapToData).collect(Collectors.toList());
-            discoverySyncData.setUpstreamDataList(discoveryUpstreamDataList);
-            return discoverySyncData;
-        }).filter(Objects::nonNull).collect(Collectors.toList());
+        }
+        return result;
     }
 
     @Override
diff --git a/shenyu-admin/src/main/resources/mappers/discovery-rel-sqlmap.xml 
b/shenyu-admin/src/main/resources/mappers/discovery-rel-sqlmap.xml
index 34534ec01e..25b02fd9ae 100644
--- a/shenyu-admin/src/main/resources/mappers/discovery-rel-sqlmap.xml
+++ b/shenyu-admin/src/main/resources/mappers/discovery-rel-sqlmap.xml
@@ -45,6 +45,21 @@
         WHERE id = #{id, jdbcType=VARCHAR}
     </select>
 
+    <select id="selectByDiscoveryHandlerIds" resultMap="BaseResultMap">
+        SELECT <include refid="Base_Column_List"/>
+        FROM discovery_rel
+        WHERE
+        <choose>
+            <when test="handlerIds != null and handlerIds.size() > 0">
+                discovery_handler_id IN
+                <foreach collection="handlerIds" item="handlerId" open="(" 
separator="," close=")">
+                    #{handlerId}
+                </foreach>
+            </when>
+            <otherwise>1 = 0</otherwise>
+        </choose>
+    </select>
+
     <select id="selectAll" resultMap="BaseResultMap">
         SELECT
         <include refid="Base_Column_List"/>
diff --git 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/mapper/DiscoveryBatchQueryTest.java
 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/mapper/DiscoveryBatchQueryTest.java
new file mode 100644
index 0000000000..6e5e130d27
--- /dev/null
+++ 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/mapper/DiscoveryBatchQueryTest.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package org.apache.shenyu.admin.mapper;
+
+import org.apache.ibatis.builder.xml.XMLMapperBuilder;
+import org.apache.ibatis.mapping.Environment;
+import org.apache.ibatis.session.Configuration;
+import org.apache.ibatis.session.SqlSession;
+import org.apache.ibatis.session.SqlSessionFactoryBuilder;
+import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
+import org.junit.jupiter.api.Test;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.datasource.DriverManagerDataSource;
+import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
+
+import java.io.InputStream;
+import java.util.List;
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class DiscoveryBatchQueryTest {
+
+    @Test
+    void readsOnlyRowsForRequestedHandlers() throws Exception {
+        DriverManagerDataSource dataSource = new DriverManagerDataSource(
+                "jdbc:h2:mem:discovery-batch-" + UUID.randomUUID() + 
";DB_CLOSE_DELAY=-1;MODE=MySQL", "sa", "");
+        JdbcTemplate jdbc = new JdbcTemplate(dataSource);
+        try {
+            new ResourceDatabasePopulator(new 
ClassPathResource("sql-script/h2/schema.sql")).execute(dataSource);
+            for (String id : List.of("a", "b", "c")) {
+                jdbc.update("INSERT INTO discovery_rel (id, plugin_name, 
discovery_handler_id, selector_id) VALUES (?, 'divide', ?, ?)", id, id, "s-" + 
id);
+                jdbc.update("INSERT INTO discovery_upstream (id, 
discovery_handler_id, namespace_id, upstream_url, upstream_status, weight) "
+                        + "VALUES (?, ?, 'namespace', 'localhost:8080', 0, 
50)", id, id);
+            }
+            Configuration configuration = new Configuration(new 
Environment("test", new JdbcTransactionFactory(), dataSource));
+            for (String mapper : List.of("discovery-rel", 
"discovery-upstream")) {
+                try (InputStream input = new ClassPathResource("mappers/" + 
mapper + "-sqlmap.xml").getInputStream()) {
+                    new XMLMapperBuilder(input, configuration, mapper, 
configuration.getSqlFragments()).parse();
+                }
+            }
+            try (SqlSession session = new 
SqlSessionFactoryBuilder().build(configuration).openSession()) {
+                DiscoveryRelMapper relations = 
session.getMapper(DiscoveryRelMapper.class);
+                DiscoveryUpstreamMapper upstreams = 
session.getMapper(DiscoveryUpstreamMapper.class);
+                assertEquals(2, 
relations.selectByDiscoveryHandlerIds(List.of("a", "b")).size());
+                assertEquals(2, 
upstreams.selectByDiscoveryHandlerIds(List.of("a", "b")).size());
+                assertEquals("s-c", 
relations.selectByDiscoveryHandlerIds(List.of("c")).get(0).getSelectorId());
+                assertEquals("c", 
upstreams.selectByDiscoveryHandlerIds(List.of("c")).get(0).getDiscoveryHandlerId());
+                
assertTrue(relations.selectByDiscoveryHandlerIds(List.of("missing")).isEmpty());
+                
assertTrue(upstreams.selectByDiscoveryHandlerIds(List.of("missing")).isEmpty());
+                
assertTrue(upstreams.selectByDiscoveryHandlerIds(List.of()).isEmpty());
+                
assertTrue(upstreams.selectByDiscoveryHandlerIds(null).isEmpty());
+                
assertTrue(relations.selectByDiscoveryHandlerIds(List.of()).isEmpty());
+                
assertTrue(relations.selectByDiscoveryHandlerIds(null).isEmpty());
+            }
+        } finally {
+            jdbc.execute("SHUTDOWN");
+        }
+    }
+}
diff --git 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/DiscoveryUpstreamServiceTest.java
 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/DiscoveryUpstreamServiceTest.java
index 7f4f0e66cd..0cfd83239b 100644
--- 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/DiscoveryUpstreamServiceTest.java
+++ 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/DiscoveryUpstreamServiceTest.java
@@ -58,7 +58,10 @@ import org.mockito.junit.jupiter.MockitoExtension;
 import java.sql.Timestamp;
 import java.time.LocalDateTime;
 import java.util.Collections;
+import java.util.ArrayList;
 import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
 import java.util.Locale;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -69,9 +72,10 @@ import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.BDDMockito.given;
 import static org.mockito.Mockito.when;
 import org.springframework.test.util.ReflectionTestUtils;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verifyNoInteractions;
-import static org.mockito.Mockito.never;
 
 /**
  * Test cases for DiscoveryUpstreamService.
@@ -207,12 +211,135 @@ public final class DiscoveryUpstreamServiceTest {
         List<DiscoveryHandlerDO> list = 
Collections.singletonList(buildDiscoveryHandlerDO());
 
         when(discoveryHandlerMapper.selectAll()).thenReturn(list);
-        
when(discoveryRelMapper.selectByDiscoveryHandlerId(any())).thenReturn(buildDiscoveryRelDO());
-        
when(proxySelectorMapper.selectById(any())).thenReturn(buildProxySelectorDO());
+        DiscoveryRelDO relation = buildDiscoveryRelDO();
+        relation.setDiscoveryHandlerId("123");
+        relation.setProxySelectorId("selector_1");
+        
when(discoveryRelMapper.selectByDiscoveryHandlerIds(any())).thenReturn(Collections.singletonList(relation));
+        
when(proxySelectorMapper.selectByIds(any())).thenReturn(Collections.singletonList(buildProxySelectorDO()));
         List<DiscoverySyncData> dataList = discoveryUpstreamService.listAll();
         assertEquals(dataList.size(), list.size());
     }
 
+    @ParameterizedTest
+    @ValueSource(ints = {20, 501})
+    void batchesQueriesAndPreservesHandlerOrder(final int count) {
+        List<DiscoveryHandlerDO> handlers = new ArrayList<>();
+        for (int i = 0; i < count; i++) {
+            DiscoveryHandlerDO handler = buildDiscoveryHandlerDO();
+            handler.setId(String.valueOf(i));
+            handlers.add(handler);
+        }
+        when(discoveryHandlerMapper.selectAll()).thenReturn(handlers);
+        
when(discoveryRelMapper.selectByDiscoveryHandlerIds(any())).thenAnswer(invocation
 -> {
+            List<String> ids = invocation.getArgument(0);
+            Assertions.assertTrue(ids.size() <= 500);
+            return ids.stream().map(id -> {
+                DiscoveryRelDO relation = buildDiscoveryRelDO();
+                relation.setDiscoveryHandlerId(id);
+                if (Integer.parseInt(id) % 2 == 0) {
+                    relation.setSelectorId(id);
+                } else {
+                    relation.setProxySelectorId(id);
+                }
+                return relation;
+            }).collect(Collectors.toList());
+        });
+        when(selectorMapper.selectByIdSet(any())).thenAnswer(invocation -> {
+            java.util.Set<String> ids = invocation.getArgument(0);
+            return ids.stream().map(id -> {
+                SelectorDO selector = buildSelectorDO();
+                selector.setId(id);
+                return selector;
+            }).collect(Collectors.toList());
+        });
+        when(proxySelectorMapper.selectByIds(any())).thenAnswer(invocation -> {
+            List<String> ids = invocation.getArgument(0);
+            return ids.stream().map(id -> {
+                ProxySelectorDO proxy = buildProxySelectorDO();
+                proxy.setId(id);
+                return proxy;
+            }).collect(Collectors.toList());
+        });
+        
when(discoveryUpstreamMapper.selectByDiscoveryHandlerIds(any())).thenAnswer(invocation
 -> {
+            List<String> ids = invocation.getArgument(0);
+            return ids.stream().map(id -> buildDiscoveryUpstreamDO("u-" + id, 
id, "localhost:8080")).collect(Collectors.toList());
+        });
+        List<DiscoverySyncData> data = discoveryUpstreamService.listAll();
+        assertEquals(count, data.size());
+        for (int i = 0; i < count; i++) {
+            assertEquals(String.valueOf(i), data.get(i).getSelectorId());
+            assertEquals(1, data.get(i).getUpstreamDataList().size());
+        }
+        int batches = (count + 499) / 500;
+        verify(discoveryHandlerMapper).selectAll();
+        verify(discoveryRelMapper, 
times(batches)).selectByDiscoveryHandlerIds(any());
+        verify(discoveryUpstreamMapper, 
times(batches)).selectByDiscoveryHandlerIds(any());
+        verify(selectorMapper, times(batches)).selectByIdSet(any());
+        verify(proxySelectorMapper, times(count / 500 + (count % 500 > 1 ? 1 : 
0))).selectByIds(any());
+        verify(discoveryRelMapper, never()).selectByDiscoveryHandlerId(any());
+        verify(discoveryUpstreamMapper, 
never()).selectByDiscoveryHandlerId(any());
+        verify(selectorMapper, never()).selectById(any());
+        verify(proxySelectorMapper, never()).selectById(any());
+    }
+
+    @Test
+    public void duplicateAndMissingBindingsDoNotAbortOtherHandlers() {
+        List<DiscoveryHandlerDO> handlers = new ArrayList<>();
+        for (String id : List.of("valid", "missing-relation", 
"missing-selector", "missing-proxy")) {
+            DiscoveryHandlerDO handler = buildDiscoveryHandlerDO();
+            handler.setId(id);
+            handlers.add(handler);
+        }
+        DiscoveryRelDO valid = buildDiscoveryRelDO();
+        valid.setDiscoveryHandlerId("valid");
+        valid.setSelectorId("selector_1");
+        DiscoveryRelDO duplicate = buildDiscoveryRelDO();
+        duplicate.setDiscoveryHandlerId("valid");
+        duplicate.setSelectorId("missing");
+        DiscoveryRelDO missingSelector = buildDiscoveryRelDO();
+        missingSelector.setDiscoveryHandlerId("missing-selector");
+        missingSelector.setSelectorId("missing");
+        DiscoveryRelDO missingProxy = buildDiscoveryRelDO();
+        missingProxy.setDiscoveryHandlerId("missing-proxy");
+        missingProxy.setProxySelectorId("missing");
+        when(discoveryHandlerMapper.selectAll()).thenReturn(handlers);
+        
when(discoveryRelMapper.selectByDiscoveryHandlerIds(any())).thenReturn(List.of(valid,
 duplicate, missingSelector, missingProxy));
+        
when(selectorMapper.selectByIdSet(any())).thenReturn(List.of(buildSelectorDO()));
+        
when(proxySelectorMapper.selectByIds(any())).thenReturn(Collections.emptyList());
+        
when(discoveryUpstreamMapper.selectByDiscoveryHandlerIds(any())).thenReturn(Collections.emptyList());
+
+        List<DiscoverySyncData> data = discoveryUpstreamService.listAll();
+
+        assertEquals(1, data.size());
+        assertEquals("selector_1", data.get(0).getSelectorId());
+    }
+
+    @ParameterizedTest
+    @ValueSource(booleans = {true, false})
+    void batchesNamespaceSyncAndRetainsNamespace(final boolean proxy) {
+        final DiscoveryRelDO relation = buildDiscoveryRelDO();
+        relation.setDiscoveryHandlerId("123");
+        
when(discoveryHandlerMapper.selectAllByNamespaceId("tenant")).thenReturn(List.of(buildDiscoveryHandlerDO()));
+        if (proxy) {
+            relation.setSelectorId("");
+            relation.setProxySelectorId("selector_1");
+            ProxySelectorDO selector = buildProxySelectorDO();
+            selector.setNamespaceId("tenant");
+            
when(proxySelectorMapper.selectByIds(List.of("selector_1"))).thenReturn(List.of(selector));
+        } else {
+            relation.setSelectorId("selector_1");
+            SelectorDO selector = buildSelectorDO();
+            selector.setNamespaceId("tenant");
+            
when(selectorMapper.selectByIdSet(Set.of("selector_1"))).thenReturn(List.of(selector));
+        }
+        
when(discoveryRelMapper.selectByDiscoveryHandlerIds(List.of("123"))).thenReturn(List.of(relation));
+        List<DiscoverySyncData> data = 
discoveryUpstreamService.listAllByNamespaceId("tenant");
+        assertEquals(1, data.size());
+        assertEquals("tenant", data.get(0).getNamespaceId());
+        verify(discoveryHandlerMapper, never()).selectAll();
+        verify(discoveryRelMapper, never()).selectByDiscoveryHandlerId(any());
+    }
+
     @Test
     public void testListAllSkipsOrphansAndKeepsValidBindings() {
         DiscoveryHandlerDO noRelation = buildDiscoveryHandlerDO();
@@ -226,23 +353,23 @@ public final class DiscoveryUpstreamServiceTest {
         DiscoveryHandlerDO validProxy = buildDiscoveryHandlerDO();
         validProxy.setId("valid-proxy");
         
when(discoveryHandlerMapper.selectAll()).thenReturn(List.of(noRelation, 
missingSelector, validSelector, missingProxy, validProxy));
-        
when(discoveryRelMapper.selectByDiscoveryHandlerId("no-relation")).thenReturn(null);
         DiscoveryRelDO staleSelectorRel = buildDiscoveryRelDO();
         staleSelectorRel.setSelectorId("deleted-selector");
-        
when(discoveryRelMapper.selectByDiscoveryHandlerId("missing-selector")).thenReturn(staleSelectorRel);
+        staleSelectorRel.setDiscoveryHandlerId("missing-selector");
         DiscoveryRelDO staleProxyRel = buildDiscoveryRelDO();
         staleProxyRel.setProxySelectorId("deleted-proxy");
-        
when(discoveryRelMapper.selectByDiscoveryHandlerId("missing-proxy")).thenReturn(staleProxyRel);
+        staleProxyRel.setDiscoveryHandlerId("missing-proxy");
         DiscoveryRelDO selectorRel = buildDiscoveryRelDO();
         selectorRel.setSelectorId("selector_1");
-        
when(discoveryRelMapper.selectByDiscoveryHandlerId("valid-selector")).thenReturn(selectorRel);
-        
when(selectorMapper.selectById("selector_1")).thenReturn(buildSelectorDO());
-        when(selectorMapper.selectById("deleted-selector")).thenReturn(null);
+        selectorRel.setDiscoveryHandlerId("valid-selector");
+        when(selectorMapper.selectByIdSet(Set.of("selector_1", 
"deleted-selector"))).thenReturn(List.of(buildSelectorDO()));
         DiscoveryRelDO proxyRel = buildDiscoveryRelDO();
         proxyRel.setProxySelectorId("proxy_1");
-        
when(discoveryRelMapper.selectByDiscoveryHandlerId("valid-proxy")).thenReturn(proxyRel);
-        
when(proxySelectorMapper.selectById("proxy_1")).thenReturn(buildProxySelectorDO());
-        when(proxySelectorMapper.selectById("deleted-proxy")).thenReturn(null);
+        proxyRel.setDiscoveryHandlerId("valid-proxy");
+        
when(discoveryRelMapper.selectByDiscoveryHandlerIds(any())).thenReturn(List.of(staleSelectorRel,
 staleProxyRel, selectorRel, proxyRel));
+        ProxySelectorDO proxy = buildProxySelectorDO();
+        proxy.setId("proxy_1");
+        when(proxySelectorMapper.selectByIds(List.of("deleted-proxy", 
"proxy_1"))).thenReturn(List.of(proxy));
         List<DiscoverySyncData> result = discoveryUpstreamService.listAll();
         assertEquals(2, result.size());
         assertEquals("selector_1", result.get(0).getSelectorId());
@@ -381,7 +508,7 @@ public final class DiscoveryUpstreamServiceTest {
     }
 
     @Test
-    public void testRolledBackBatchDoesNotPublish() {
+    public void testRolledBackUpdateBatchDoesNotPublish() {
         TransactionSynchronizationManager.initSynchronization();
         try {
             discoveryUpstreamService.updateBatch("123", 
Collections.emptyList());

Reply via email to