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 ce93f58f0d fix: prevent duplicate TCP bootstrap server creation (#7012)
ce93f58f0d is described below

commit ce93f58f0d8169728e260dd5dcad3acc7c97ec87
Author: lymerin <[email protected]>
AuthorDate: Wed Sep 2 22:16:15 2026 +0800

    fix: prevent duplicate TCP bootstrap server creation (#7012)
    
    Co-authored-by: aias00 <[email protected]>
---
 .../plugin/tcp/handler/TcpBootstrapFactory.java    |  82 +++++++++-
 .../tcp/handler/TcpProxySelectorDataHandler.java   |  33 ++--
 .../tcp/handler/TcpBootstrapFactoryTest.java       | 177 +++++++++++++++++++++
 .../shenyu/protocol/tcp/TcpBootstrapServer.java    |  60 +++++--
 .../protocol/tcp/TcpBootstrapServerTest.java       |  67 +++++++-
 5 files changed, 383 insertions(+), 36 deletions(-)

diff --git 
a/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-tcp/src/main/java/org/apache/shenyu/plugin/tcp/handler/TcpBootstrapFactory.java
 
b/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-tcp/src/main/java/org/apache/shenyu/plugin/tcp/handler/TcpBootstrapFactory.java
index 070ada7064..fe9f43a5d6 100644
--- 
a/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-tcp/src/main/java/org/apache/shenyu/plugin/tcp/handler/TcpBootstrapFactory.java
+++ 
b/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-tcp/src/main/java/org/apache/shenyu/plugin/tcp/handler/TcpBootstrapFactory.java
@@ -21,11 +21,16 @@ import com.google.common.eventbus.EventBus;
 import org.apache.shenyu.protocol.tcp.BootstrapServer;
 import org.apache.shenyu.protocol.tcp.TcpBootstrapServer;
 import org.apache.shenyu.protocol.tcp.TcpServerConfiguration;
+import org.apache.shenyu.protocol.tcp.UpstreamProvider;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.util.Map;
+import java.util.Collections;
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
 
 /**
  * TcpBootstrapFactory.
@@ -36,7 +41,9 @@ public final class TcpBootstrapFactory {
 
     private static final TcpBootstrapFactory SINGLETON = new 
TcpBootstrapFactory();
 
-    private final Map<String, BootstrapServer> cache = new 
ConcurrentHashMap<>();
+    private final ConcurrentMap<String, BootstrapServer> cache = new 
ConcurrentHashMap<>();
+
+    private final ConcurrentMap<String, CompletableFuture<BootstrapServer>> 
creations = new ConcurrentHashMap<>();
 
     private TcpBootstrapFactory() {
     }
@@ -63,6 +70,62 @@ public final class TcpBootstrapFactory {
         return bootstrapServer;
     }
 
+    /**
+     * Create and cache a bootstrap server if absent.
+     *
+     * @param configuration configuration
+     * @return true if a bootstrap server was created
+     */
+    public boolean createBootstrapServerIfAbsent(final TcpServerConfiguration 
configuration) {
+        String selectorName = configuration.getPluginSelectorName();
+        if (cache.containsKey(selectorName)) {
+            return false;
+        }
+        CompletableFuture<BootstrapServer> creation = new 
CompletableFuture<>();
+        CompletableFuture<BootstrapServer> existingCreation = 
creations.putIfAbsent(selectorName, creation);
+        if (Objects.nonNull(existingCreation)) {
+            awaitCreation(existingCreation);
+            return false;
+        }
+        try {
+            BootstrapServer cachedServer = cache.get(selectorName);
+            if (Objects.nonNull(cachedServer)) {
+                creation.complete(cachedServer);
+                return false;
+            }
+            UpstreamProvider.getSingleton().createUpstreams(selectorName, 
Collections.emptyList());
+            BootstrapServer bootstrapServer = 
createBootstrapServer(configuration);
+            BootstrapServer existingServer = cache.putIfAbsent(selectorName, 
bootstrapServer);
+            if (Objects.nonNull(existingServer)) {
+                bootstrapServer.shutdown();
+                creation.complete(existingServer);
+                return false;
+            }
+            creation.complete(bootstrapServer);
+            return true;
+        } catch (RuntimeException ex) {
+            creation.completeExceptionally(ex);
+            throw ex;
+        } finally {
+            creations.remove(selectorName, creation);
+        }
+    }
+
+    private static void awaitCreation(final CompletableFuture<BootstrapServer> 
creation) {
+        try {
+            creation.join();
+        } catch (CompletionException ex) {
+            Throwable cause = ex.getCause();
+            if (cause instanceof RuntimeException) {
+                throw (RuntimeException) cause;
+            }
+            if (cause instanceof Error) {
+                throw (Error) cause;
+            }
+            throw ex;
+        }
+    }
+
     /**
      * cache bootstrapServer by selectorName.
      *
@@ -93,6 +156,21 @@ public final class TcpBootstrapFactory {
         return cache.remove(selectorName);
     }
 
+    /**
+     * Remove and shutdown a bootstrap server.
+     *
+     * @param selectorName selectorName
+     * @return true if a bootstrap server was removed
+     */
+    public boolean removeAndShutdown(final String selectorName) {
+        BootstrapServer bootstrapServer = cache.remove(selectorName);
+        if (Objects.isNull(bootstrapServer)) {
+            return false;
+        }
+        bootstrapServer.shutdown();
+        return true;
+    }
+
     /**
      * Clear cache.
      */
diff --git 
a/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-tcp/src/main/java/org/apache/shenyu/plugin/tcp/handler/TcpProxySelectorDataHandler.java
 
b/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-tcp/src/main/java/org/apache/shenyu/plugin/tcp/handler/TcpProxySelectorDataHandler.java
index 78d4f2aefd..b39bdce1d1 100644
--- 
a/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-tcp/src/main/java/org/apache/shenyu/plugin/tcp/handler/TcpProxySelectorDataHandler.java
+++ 
b/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-tcp/src/main/java/org/apache/shenyu/plugin/tcp/handler/TcpProxySelectorDataHandler.java
@@ -20,15 +20,10 @@ package org.apache.shenyu.plugin.tcp.handler;
 import org.apache.shenyu.common.dto.ProxySelectorData;
 import org.apache.shenyu.common.enums.PluginEnum;
 import org.apache.shenyu.plugin.base.handler.ProxySelectorDataHandler;
-import org.apache.shenyu.protocol.tcp.BootstrapServer;
 import org.apache.shenyu.protocol.tcp.TcpServerConfiguration;
-import org.apache.shenyu.protocol.tcp.UpstreamProvider;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.util.Collections;
-import java.util.Objects;
-
 public class TcpProxySelectorDataHandler implements ProxySelectorDataHandler {
 
     private static final Logger LOG = 
LoggerFactory.getLogger(TcpProxySelectorDataHandler.class);
@@ -36,26 +31,26 @@ public class TcpProxySelectorDataHandler implements 
ProxySelectorDataHandler {
     @Override
     public void handlerProxySelector(final ProxySelectorData 
proxySelectorData) {
         String name = proxySelectorData.getName();
-        if (!TcpBootstrapFactory.getSingleton().inCache(name)) {
-            Integer forwardPort = proxySelectorData.getForwardPort();
-            TcpServerConfiguration tcpServerConfiguration = new 
TcpServerConfiguration();
-            tcpServerConfiguration.setPort(forwardPort);
-            tcpServerConfiguration.setProps(proxySelectorData.getProps());
-            tcpServerConfiguration.setPluginSelectorName(name);
-            UpstreamProvider.getSingleton().createUpstreams(name, 
Collections.emptyList());
-            BootstrapServer bootstrapServer = 
TcpBootstrapFactory.getSingleton().createBootstrapServer(tcpServerConfiguration);
-            TcpBootstrapFactory.getSingleton().cache(name, bootstrapServer);
-            LOG.info("shenyu create TcpBootstrapServer success name is {} port 
is {}", proxySelectorData.getName(), forwardPort);
+        TcpBootstrapFactory factory = TcpBootstrapFactory.getSingleton();
+        if (factory.inCache(name)) {
+            LOG.info("shenyu already created TcpBootstrapServer name is {} 
port is {}", name, proxySelectorData.getForwardPort());
+            return;
+        }
+        Integer forwardPort = proxySelectorData.getForwardPort();
+        TcpServerConfiguration tcpServerConfiguration = new 
TcpServerConfiguration();
+        tcpServerConfiguration.setPort(forwardPort);
+        tcpServerConfiguration.setProps(proxySelectorData.getProps());
+        tcpServerConfiguration.setPluginSelectorName(name);
+        if (factory.createBootstrapServerIfAbsent(tcpServerConfiguration)) {
+            LOG.info("shenyu create TcpBootstrapServer success name is {} port 
is {}", name, forwardPort);
         } else {
-            LOG.info("shenyu already created TcpBootstrapServer name is {} 
port is {}", proxySelectorData.getName(), proxySelectorData.getForwardPort());
+            LOG.info("shenyu already created TcpBootstrapServer name is {} 
port is {}", name, forwardPort);
         }
     }
 
     @Override
     public void removeProxySelector(final String proxySelectorName) {
-        BootstrapServer bootstrapServer = 
TcpBootstrapFactory.getSingleton().removeCache(proxySelectorName);
-        if (Objects.nonNull(bootstrapServer)) {
-            bootstrapServer.shutdown();
+        if 
(TcpBootstrapFactory.getSingleton().removeAndShutdown(proxySelectorName)) {
             LOG.info("shenyu shutdown {}", proxySelectorName);
         }
     }
diff --git 
a/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-tcp/src/test/java/org/apache/shenyu/plugin/tcp/handler/TcpBootstrapFactoryTest.java
 
b/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-tcp/src/test/java/org/apache/shenyu/plugin/tcp/handler/TcpBootstrapFactoryTest.java
new file mode 100644
index 0000000000..6bbc02fd72
--- /dev/null
+++ 
b/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-tcp/src/test/java/org/apache/shenyu/plugin/tcp/handler/TcpBootstrapFactoryTest.java
@@ -0,0 +1,177 @@
+/*
+ * 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.plugin.tcp.handler;
+
+import org.apache.shenyu.protocol.tcp.BootstrapServer;
+import org.apache.shenyu.protocol.tcp.TcpServerConfiguration;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.net.ServerSocket;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+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.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+public final class TcpBootstrapFactoryTest {
+
+    private static final String FIRST_SELECTOR = "first";
+
+    private static final String SECOND_SELECTOR = "second";
+
+    private final TcpBootstrapFactory factory = 
TcpBootstrapFactory.getSingleton();
+
+    @BeforeEach
+    public void setUp() {
+        factory.clearCache();
+    }
+
+    @AfterEach
+    public void tearDown() {
+        factory.clearCache();
+    }
+
+    @Test
+    public void shouldCreateServerOnlyOnceForConcurrentSameSelector() throws 
Exception {
+        int threadCount = 16;
+        TcpServerConfiguration configuration = configuration(FIRST_SELECTOR, 
getFreePort());
+        ExecutorService executor = Executors.newFixedThreadPool(threadCount);
+        CountDownLatch ready = new CountDownLatch(threadCount);
+        CountDownLatch start = new CountDownLatch(1);
+        List<Future<Boolean>> results = new ArrayList<>();
+        try {
+            for (int index = 0; index < threadCount; index++) {
+                results.add(executor.submit(() -> {
+                    ready.countDown();
+                    start.await();
+                    return 
factory.createBootstrapServerIfAbsent(configuration);
+                }));
+            }
+            assertTrue(ready.await(5, TimeUnit.SECONDS));
+            start.countDown();
+            int createdCount = 0;
+            for (Future<Boolean> result : results) {
+                if (result.get(30, TimeUnit.SECONDS)) {
+                    createdCount++;
+                }
+            }
+            assertEquals(1, createdCount);
+            assertNotNull(factory.getCache(FIRST_SELECTOR));
+        } finally {
+            start.countDown();
+            executor.shutdownNow();
+        }
+    }
+
+    @Test
+    public void shouldAllowRetryAfterCreationFailure() throws IOException {
+        TcpServerConfiguration configuration;
+        try (ServerSocket occupiedPort = new ServerSocket(0)) {
+            configuration = configuration(FIRST_SELECTOR, 
occupiedPort.getLocalPort());
+            assertThrows(RuntimeException.class, () -> 
factory.createBootstrapServerIfAbsent(configuration));
+            assertNull(factory.getCache(FIRST_SELECTOR));
+        }
+
+        configuration.setPort(getFreePort());
+        assertTrue(factory.createBootstrapServerIfAbsent(configuration));
+        assertNotNull(factory.getCache(FIRST_SELECTOR));
+    }
+
+    @Test
+    public void shouldUnwrapFailureFromExistingCreation() throws Exception {
+        IllegalStateException failure = new IllegalStateException("creation 
failed");
+        CompletableFuture<BootstrapServer> failedCreation = new 
CompletableFuture<>();
+        failedCreation.completeExceptionally(failure);
+        ConcurrentMap<String, CompletableFuture<BootstrapServer>> creations = 
getCreations();
+        creations.put(FIRST_SELECTOR, failedCreation);
+        try {
+            IllegalStateException actual = 
assertThrows(IllegalStateException.class,
+                    () -> 
factory.createBootstrapServerIfAbsent(configuration(FIRST_SELECTOR, 0)));
+            assertSame(failure, actual);
+        } finally {
+            creations.remove(FIRST_SELECTOR, failedCreation);
+        }
+    }
+
+    @Test
+    public void shouldNotBlockDifferentSelectorRemovalDuringShutdown() throws 
Exception {
+        BootstrapServer blockingServer = mock(BootstrapServer.class);
+        BootstrapServer secondServer = mock(BootstrapServer.class);
+        CountDownLatch shutdownStarted = new CountDownLatch(1);
+        CountDownLatch releaseShutdown = new CountDownLatch(1);
+        doAnswer(invocation -> {
+            shutdownStarted.countDown();
+            assertTrue(releaseShutdown.await(5, TimeUnit.SECONDS));
+            return null;
+        }).when(blockingServer).shutdown();
+        factory.cache(FIRST_SELECTOR, blockingServer);
+        factory.cache(SECOND_SELECTOR, secondServer);
+
+        ExecutorService executor = Executors.newFixedThreadPool(2);
+        try {
+            final Future<Boolean> firstResult = executor.submit(() -> 
factory.removeAndShutdown(FIRST_SELECTOR));
+            assertTrue(shutdownStarted.await(5, TimeUnit.SECONDS));
+            Future<Boolean> secondResult = executor.submit(() -> 
factory.removeAndShutdown(SECOND_SELECTOR));
+            assertTrue(secondResult.get(5, TimeUnit.SECONDS));
+            verify(secondServer).shutdown();
+            releaseShutdown.countDown();
+            assertTrue(firstResult.get(5, TimeUnit.SECONDS));
+        } finally {
+            releaseShutdown.countDown();
+            executor.shutdownNow();
+        }
+    }
+
+    private static TcpServerConfiguration configuration(final String 
selectorName, final int port) {
+        TcpServerConfiguration configuration = new TcpServerConfiguration();
+        configuration.setPluginSelectorName(selectorName);
+        configuration.setPort(port);
+        return configuration;
+    }
+
+    private static int getFreePort() throws IOException {
+        try (ServerSocket socket = new ServerSocket(0)) {
+            return socket.getLocalPort();
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private ConcurrentMap<String, CompletableFuture<BootstrapServer>> 
getCreations() throws Exception {
+        Field field = TcpBootstrapFactory.class.getDeclaredField("creations");
+        field.setAccessible(true);
+        return (ConcurrentMap<String, CompletableFuture<BootstrapServer>>) 
field.get(factory);
+    }
+}
diff --git 
a/shenyu-protocol/shenyu-protocol-tcp/src/main/java/org/apache/shenyu/protocol/tcp/TcpBootstrapServer.java
 
b/shenyu-protocol/shenyu-protocol-tcp/src/main/java/org/apache/shenyu/protocol/tcp/TcpBootstrapServer.java
index b7a48b6597..df58619334 100644
--- 
a/shenyu-protocol/shenyu-protocol-tcp/src/main/java/org/apache/shenyu/protocol/tcp/TcpBootstrapServer.java
+++ 
b/shenyu-protocol/shenyu-protocol-tcp/src/main/java/org/apache/shenyu/protocol/tcp/TcpBootstrapServer.java
@@ -56,6 +56,8 @@ public class TcpBootstrapServer implements BootstrapServer {
 
     private final EventBus eventBus;
 
+    private boolean disposed;
+
     public TcpBootstrapServer(final EventBus eventBus) {
         this.eventBus = eventBus;
     }
@@ -71,15 +73,24 @@ public class TcpBootstrapServer implements BootstrapServer {
         connectionContext.init(tcpServerConfiguration.getProps());
         loopResources = LoopResources.create("shenyu-tcp-bootstrap-server-" + 
tcpServerConfiguration.getPort(), Integer.parseInt(bossGroupThreadCount),
                 Integer.parseInt(workerGroupThreadCount), true);
-        TcpServer tcpServer = TcpServer.create()
-                .doOnChannelInit((connObserver, channel, remoteAddress) -> 
channel.pipeline().addFirst(new LoggingHandler(LogLevel.INFO)))
-                .wiretap(true)
-                .observe((c, s) -> LOG.info("connection={}|status={}", c, s))
-                //.childObserve(connectionObserver)
-                .doOnConnection(this::bridgeConnections)
-                .port(tcpServerConfiguration.getPort())
-                .runOn(loopResources);
-        server = tcpServer.bindNow();
+        try {
+            TcpServer tcpServer = TcpServer.create()
+                    .doOnChannelInit((connObserver, channel, remoteAddress) -> 
channel.pipeline().addFirst(new LoggingHandler(LogLevel.INFO)))
+                    .wiretap(true)
+                    .observe((c, s) -> LOG.info("connection={}|status={}", c, 
s))
+                    //.childObserve(connectionObserver)
+                    .doOnConnection(this::bridgeConnections)
+                    .port(tcpServerConfiguration.getPort())
+                    .runOn(loopResources);
+            server = tcpServer.bindNow();
+        } catch (RuntimeException startFailure) {
+            try {
+                loopResources.dispose();
+            } catch (RuntimeException cleanupFailure) {
+                startFailure.addSuppressed(cleanupFailure);
+            }
+            throw startFailure;
+        }
     }
 
     private void bridgeConnections(final Connection serverConn) {
@@ -125,9 +136,34 @@ public class TcpBootstrapServer implements BootstrapServer 
{
      * shutdown.
      */
     @Override
-    public void shutdown() {
-        server.disposeNow();
-        loopResources.dispose();
+    public synchronized void shutdown() {
+        if (disposed) {
+            return;
+        }
+        RuntimeException failure = null;
+        try {
+            if (Objects.nonNull(server)) {
+                server.disposeNow();
+            }
+        } catch (RuntimeException ex) {
+            failure = ex;
+        }
+        try {
+            if (Objects.nonNull(loopResources)) {
+                loopResources.dispose();
+            }
+        } catch (RuntimeException ex) {
+            if (Objects.isNull(failure)) {
+                failure = ex;
+            } else {
+                failure.addSuppressed(ex);
+            }
+        } finally {
+            disposed = true;
+        }
+        if (Objects.nonNull(failure)) {
+            throw failure;
+        }
     }
 
 }
diff --git 
a/shenyu-protocol/shenyu-protocol-tcp/src/test/java/org/apache/shenyu/protocol/tcp/TcpBootstrapServerTest.java
 
b/shenyu-protocol/shenyu-protocol-tcp/src/test/java/org/apache/shenyu/protocol/tcp/TcpBootstrapServerTest.java
index d5c938a272..76f393a5d4 100644
--- 
a/shenyu-protocol/shenyu-protocol-tcp/src/test/java/org/apache/shenyu/protocol/tcp/TcpBootstrapServerTest.java
+++ 
b/shenyu-protocol/shenyu-protocol-tcp/src/test/java/org/apache/shenyu/protocol/tcp/TcpBootstrapServerTest.java
@@ -31,20 +31,26 @@ import org.mockito.junit.jupiter.MockitoExtension;
 import reactor.core.Disposable;
 import reactor.core.publisher.Mono;
 import reactor.netty.Connection;
+import reactor.netty.DisposableServer;
+import reactor.netty.resources.LoopResources;
 
-import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.net.InetSocketAddress;
+import java.net.ServerSocket;
 import java.net.SocketAddress;
 
+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.assertSame;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
-
-import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -197,6 +203,55 @@ public class TcpBootstrapServerTest {
         assertNotNull(contextCaptor.getValue());
     }
 
+    @Test
+    void shouldDisposeLoopResourcesWhenBindFails() throws Exception {
+        TcpBootstrapServer bootstrapServer = new TcpBootstrapServer(new 
EventBus());
+        try (ServerSocket occupiedPort = new ServerSocket(0)) {
+            TcpServerConfiguration configuration = new 
TcpServerConfiguration();
+            configuration.setPluginSelectorName("bind-failure");
+            configuration.setPort(occupiedPort.getLocalPort());
+
+            assertThrows(RuntimeException.class, () -> 
bootstrapServer.start(configuration));
+            LoopResources loopResources = getField(bootstrapServer, 
"loopResources", LoopResources.class);
+            assertNotNull(loopResources);
+            assertTrue(loopResources.isDisposed());
+        }
+    }
+
+    @Test
+    void shouldShutdownOnlyOnce() throws Exception {
+        DisposableServer disposableServer = mock(DisposableServer.class);
+        LoopResources loopResources = mock(LoopResources.class);
+        setField("server", disposableServer);
+        setField("loopResources", loopResources);
+
+        server.shutdown();
+        server.shutdown();
+
+        verify(disposableServer).disposeNow();
+        verify(loopResources).dispose();
+    }
+
+    @Test
+    void shouldDisposeLoopResourcesWhenServerDisposalFails() throws Exception {
+        DisposableServer disposableServer = mock(DisposableServer.class);
+        LoopResources loopResources = mock(LoopResources.class);
+        IllegalStateException serverFailure = new 
IllegalStateException("server disposal failed");
+        IllegalStateException loopFailure = new IllegalStateException("loop 
disposal failed");
+        doThrow(serverFailure).when(disposableServer).disposeNow();
+        doThrow(loopFailure).when(loopResources).dispose();
+        setField("server", disposableServer);
+        setField("loopResources", loopResources);
+
+        IllegalStateException actual = 
assertThrows(IllegalStateException.class, server::shutdown);
+
+        assertSame(serverFailure, actual);
+        assertEquals(1, actual.getSuppressed().length);
+        assertSame(loopFailure, actual.getSuppressed()[0]);
+        verify(loopResources).dispose();
+        assertDoesNotThrow(server::shutdown);
+    }
+
     private void invokeBridgeConnections(final Connection connection) throws 
Exception {
         Method method = 
TcpBootstrapServer.class.getDeclaredMethod("bridgeConnections", 
Connection.class);
         method.setAccessible(true);
@@ -208,4 +263,10 @@ public class TcpBootstrapServerTest {
         field.setAccessible(true);
         field.set(server, value);
     }
+
+    private static <T> T getField(final Object target, final String name, 
final Class<T> type) throws Exception {
+        Field field = target.getClass().getDeclaredField(name);
+        field.setAccessible(true);
+        return type.cast(field.get(target));
+    }
 }

Reply via email to