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 b15b655e88 fix(admin): make upstream writes transactional (#7249)
b15b655e88 is described below

commit b15b655e8809cd76c0acc64c5037e29731518f5c
Author: Liming Deng <[email protected]>
AuthorDate: Wed Sep 30 09:47:36 2026 +0800

    fix(admin): make upstream writes transactional (#7249)
---
 .../controller/DiscoveryUpstreamController.java    |   7 +-
 .../admin/service/DiscoveryUpstreamService.java    |   7 ++
 .../service/impl/DiscoveryUpstreamServiceImpl.java |  21 ++++
 .../DiscoveryUpstreamControllerTest.java           |   8 ++
 .../service/UpstreamWriteIntegrationTest.java      | 134 +++++++++++++++++++++
 5 files changed, 171 insertions(+), 6 deletions(-)

diff --git 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/controller/DiscoveryUpstreamController.java
 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/controller/DiscoveryUpstreamController.java
index 78a0dd4fd7..c8a5ca9267 100644
--- 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/controller/DiscoveryUpstreamController.java
+++ 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/controller/DiscoveryUpstreamController.java
@@ -17,7 +17,6 @@
 
 package org.apache.shenyu.admin.controller;
 
-import org.apache.commons.collections4.CollectionUtils;
 import org.apache.shenyu.admin.aspect.annotation.RestApi;
 import org.apache.shenyu.admin.mapper.DiscoveryHandlerMapper;
 import org.apache.shenyu.admin.model.dto.DiscoveryUpstreamDTO;
@@ -65,11 +64,7 @@ public class DiscoveryUpstreamController {
      */
     @PostMapping("batch")
     public ShenyuAdminResult createDiscoveryUpstreamList(@Valid @RequestBody 
final List<DiscoveryUpstreamDTO> discoveryUpstreamDTOList) {
-        if (CollectionUtils.isNotEmpty(discoveryUpstreamDTOList)) {
-            for (DiscoveryUpstreamDTO discoveryUpstreamDTO : 
discoveryUpstreamDTOList) {
-                discoveryUpstreamService.createOrUpdate(discoveryUpstreamDTO);
-            }
-        }
+        discoveryUpstreamService.createOrUpdateBatch(discoveryUpstreamDTOList);
         return ShenyuAdminResult.success();
     }
 
diff --git 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/DiscoveryUpstreamService.java
 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/DiscoveryUpstreamService.java
index d808c194b6..047cd1821a 100644
--- 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/DiscoveryUpstreamService.java
+++ 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/DiscoveryUpstreamService.java
@@ -36,6 +36,13 @@ public interface DiscoveryUpstreamService {
      */
     String createOrUpdate(DiscoveryUpstreamDTO discoveryUpstreamDTO);
 
+    /**
+     * Create or update upstreams in one transaction.
+     *
+     * @param upstreams upstreams to create or update
+     */
+    void createOrUpdateBatch(List<DiscoveryUpstreamDTO> upstreams);
+
 
     /**
      * updateBatch.
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 20cf8f8ff0..24675f44a4 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
@@ -49,6 +49,7 @@ import 
org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.StringUtils;
 
 import java.util.Collections;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -99,12 +100,32 @@ public class DiscoveryUpstreamServiceImpl implements 
DiscoveryUpstreamService {
      * @return the string
      */
     @Override
+    @Transactional(rollbackFor = Exception.class)
     public String createOrUpdate(final DiscoveryUpstreamDTO 
discoveryUpstreamDTO) {
 
         return StringUtils.hasLength(discoveryUpstreamDTO.getId())
                 ? update(discoveryUpstreamDTO) : create(discoveryUpstreamDTO);
     }
 
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void createOrUpdateBatch(final List<DiscoveryUpstreamDTO> 
upstreams) {
+        if (CollectionUtils.isEmpty(upstreams)) {
+            return;
+        }
+        Set<String> handlerIds = new LinkedHashSet<>();
+        for (DiscoveryUpstreamDTO upstream : upstreams) {
+            DiscoveryUpstreamDO entity = 
DiscoveryUpstreamDO.buildDiscoveryUpstreamDO(upstream);
+            if (StringUtils.hasLength(upstream.getId())) {
+                discoveryUpstreamMapper.update(entity);
+            } else {
+                discoveryUpstreamMapper.insert(entity);
+            }
+            handlerIds.add(upstream.getDiscoveryHandlerId());
+        }
+        handlerIds.forEach(this::fetchAll);
+    }
+
     @Override
     @Transactional
     public int updateBatch(final String discoveryHandlerId, final 
List<DiscoveryUpstreamDTO> discoveryUpstreamDTOList) {
diff --git 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/controller/DiscoveryUpstreamControllerTest.java
 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/controller/DiscoveryUpstreamControllerTest.java
index 847ae23a90..d31658aae5 100644
--- 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/controller/DiscoveryUpstreamControllerTest.java
+++ 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/controller/DiscoveryUpstreamControllerTest.java
@@ -46,6 +46,7 @@ import static 
org.apache.shenyu.common.constant.Constants.SYS_DEFAULT_NAMESPACE_
 import static org.hamcrest.Matchers.containsString;
 import static org.mockito.BDDMockito.given;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 import static 
org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
 import static 
org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@@ -133,6 +134,13 @@ public final class DiscoveryUpstreamControllerTest {
                 .andExpect(jsonPath("$.code").value(200));
     }
 
+    @Test
+    public void testBatchDelegatesToTransactionalService() {
+        List<DiscoveryUpstreamDTO> upstreams = List.of(buildDTO("handler"));
+        discoveryUpstreamController.createDiscoveryUpstreamList(upstreams);
+        verify(discoveryUpstreamService).createOrUpdateBatch(upstreams);
+    }
+
     private DiscoveryUpstreamDTO buildDTO(final String discoveryHandlerId) {
         DiscoveryUpstreamDTO dto = new DiscoveryUpstreamDTO();
         dto.setDiscoveryHandlerId(discoveryHandlerId);
diff --git 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/UpstreamWriteIntegrationTest.java
 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/UpstreamWriteIntegrationTest.java
new file mode 100644
index 0000000000..34995a14a9
--- /dev/null
+++ 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/UpstreamWriteIntegrationTest.java
@@ -0,0 +1,134 @@
+/*
+ * 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.service;
+
+import jakarta.annotation.Resource;
+import org.apache.shenyu.admin.AbstractSpringIntegrationTest;
+import org.apache.shenyu.admin.discovery.DiscoveryProcessor;
+import org.apache.shenyu.admin.discovery.DiscoveryProcessorHolder;
+import org.apache.shenyu.admin.model.dto.DiscoveryUpstreamDTO;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.dao.DataIntegrityViolationException;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+/**
+ * Transaction regression tests for upstream writes.
+ */
+public class UpstreamWriteIntegrationTest extends 
AbstractSpringIntegrationTest {
+
+    @Resource
+    private DiscoveryUpstreamService upstreamService;
+
+    @Resource
+    private JdbcTemplate jdbcTemplate;
+
+    @MockBean
+    private DiscoveryProcessorHolder processorHolder;
+
+    private DiscoveryProcessor processor;
+
+    @BeforeEach
+    public void setup() {
+        processor = mock(DiscoveryProcessor.class);
+        when(processorHolder.chooseProcessor("local")).thenReturn(processor);
+        jdbcTemplate.update("INSERT INTO discovery (id, discovery_name, 
discovery_level, namespace_id, discovery_type) "
+                + "VALUES ('write-test', 'write-test', '0', 'write-ns', 
'local')");
+        jdbcTemplate.update("INSERT INTO discovery_handler (id, discovery_id, 
handler) VALUES ('write-test', 'write-test', '{}')");
+        jdbcTemplate.update("INSERT INTO proxy_selector (id, name, 
plugin_name, type, forward_port, namespace_id) "
+                + "VALUES ('write-test', 'write-test', 'tcp', 'tcp', 18080, 
'write-ns')");
+        jdbcTemplate.update("INSERT INTO discovery_rel (id, plugin_name, 
discovery_handler_id, proxy_selector_id) "
+                + "VALUES ('write-test', 'tcp', 'write-test', 'write-test')");
+    }
+
+    @AfterEach
+    public void cleanup() {
+        jdbcTemplate.update("DELETE FROM discovery_upstream WHERE 
discovery_handler_id = 'write-test'");
+        jdbcTemplate.update("DELETE FROM discovery_rel WHERE id = 
'write-test'");
+        jdbcTemplate.update("DELETE FROM proxy_selector WHERE id = 
'write-test'");
+        jdbcTemplate.update("DELETE FROM discovery_handler WHERE id = 
'write-test'");
+        jdbcTemplate.update("DELETE FROM discovery WHERE id = 'write-test'");
+    }
+
+    @Test
+    public void testCreateRollsBackOnSyncFailure() {
+        failSync();
+        assertThrows(IllegalStateException.class, () -> 
upstreamService.createOrUpdate(upstream("localhost:8080")));
+        assertEquals(0, countUpstreams());
+    }
+
+    @Test
+    public void testUpdateRollsBackOnSyncFailure() {
+        upstreamService.createOrUpdate(upstream("localhost:8080"));
+        DiscoveryUpstreamDTO update = upstream("localhost:8081");
+        update.setId(jdbcTemplate.queryForObject("SELECT id FROM 
discovery_upstream WHERE discovery_handler_id = 'write-test'", String.class));
+        failSync();
+        assertThrows(IllegalStateException.class, () -> 
upstreamService.createOrUpdate(update));
+        assertEquals("localhost:8080", jdbcTemplate.queryForObject("SELECT 
upstream_url FROM discovery_upstream WHERE id = ?", String.class, 
update.getId()));
+    }
+
+    @Test
+    public void testBatchInsertFailureRollsBackBeforeSync() {
+        assertThrows(DataIntegrityViolationException.class,
+                () -> 
upstreamService.createOrUpdateBatch(List.of(upstream("localhost:8080"), 
upstream(null))));
+        assertEquals(0, countUpstreams());
+        verifyNoInteractions(processor);
+    }
+
+    @Test
+    public void testBatchSyncFailureRollsBackAllRows() {
+        failSync();
+        assertThrows(IllegalStateException.class,
+                () -> 
upstreamService.createOrUpdateBatch(List.of(upstream("localhost:8080"), 
upstream("localhost:8081"))));
+        assertEquals(0, countUpstreams());
+    }
+
+    private void failSync() {
+        doThrow(new IllegalStateException("sync 
failed")).when(processor).changeUpstream(any(), any());
+    }
+
+    private int countUpstreams() {
+        return jdbcTemplate.queryForObject("SELECT COUNT(*) FROM 
discovery_upstream WHERE discovery_handler_id = 'write-test'", Integer.class);
+    }
+
+    private DiscoveryUpstreamDTO upstream(final String url) {
+        DiscoveryUpstreamDTO dto = new DiscoveryUpstreamDTO();
+        dto.setDiscoveryHandlerId("write-test");
+        dto.setNamespaceId("write-ns");
+        dto.setProtocol("tcp");
+        dto.setUrl(url);
+        dto.setStatus(0);
+        dto.setWeight(50);
+        dto.setProps("{}");
+        return dto;
+    }
+}
+

Reply via email to