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 1b3baa856b fix(mqtt): fix duplicate startup and resource leaks on 
config updates (#7539)
1b3baa856b is described below

commit 1b3baa856b804c3db2eb623db6da8ebd007945d1
Author: lymerin <[email protected]>
AuthorDate: Sun Oct 11 22:34:36 2026 +0800

    fix(mqtt): fix duplicate startup and resource leaks on config updates 
(#7539)
---
 .../plugin/mqtt/handler/MqttPluginDataHandler.java |  49 ++--
 .../mqtt/handler/MqttPluginDataHandlerTest.java    | 322 ++++++++++++++++++---
 .../shenyu/protocol/mqtt/MqttBootstrapServer.java  |  48 ++-
 .../protocol/mqtt/MqttServerConfiguration.java     |  34 ++-
 .../protocol/mqtt/MqttBootstrapServerTest.java     | 140 ++++++++-
 5 files changed, 492 insertions(+), 101 deletions(-)

diff --git 
a/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-mqtt/src/main/java/org/apache/shenyu/plugin/mqtt/handler/MqttPluginDataHandler.java
 
b/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-mqtt/src/main/java/org/apache/shenyu/plugin/mqtt/handler/MqttPluginDataHandler.java
index 3cafba458b..9bac3c8f52 100644
--- 
a/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-mqtt/src/main/java/org/apache/shenyu/plugin/mqtt/handler/MqttPluginDataHandler.java
+++ 
b/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-mqtt/src/main/java/org/apache/shenyu/plugin/mqtt/handler/MqttPluginDataHandler.java
@@ -21,52 +21,43 @@ import org.apache.shenyu.common.dto.PluginData;
 import org.apache.shenyu.common.enums.PluginEnum;
 import org.apache.shenyu.common.utils.GsonUtils;
 import org.apache.shenyu.plugin.base.handler.PluginDataHandler;
-import org.apache.shenyu.protocol.mqtt.BootstrapServer;
 import org.apache.shenyu.protocol.mqtt.MqttBootstrapServer;
 import org.apache.shenyu.protocol.mqtt.MqttServerConfiguration;
 
-import java.net.InetAddress;
-import java.net.Socket;
+import java.util.Objects;
 
 /**
  * The type Mqtt plugin data handler.
  */
 public class MqttPluginDataHandler implements PluginDataHandler {
 
-    private final BootstrapServer server = new MqttBootstrapServer();
+    private final MqttBootstrapServer server = new MqttBootstrapServer();
+
+    private MqttServerConfiguration runningConfiguration;
 
     @Override
-    public void handlerPlugin(final PluginData pluginData) {
+    public synchronized void handlerPlugin(final PluginData pluginData) {
+        if (!pluginData.getEnabled()) {
+            server.shutdown();
+            runningConfiguration = null;
+            return;
+        }
         MqttServerConfiguration configuration = 
GsonUtils.getInstance().fromJson(pluginData.getConfig(), 
MqttServerConfiguration.class);
-        configuration.afterPropertiesSet();
-        if (pluginData.getEnabled()) {
-            server.init();
-            server.start();
-        } else {
-            if (isPortUsing(configuration.getPort())) {
-                server.shutdown();
-            }
+        if (configuration.hasSameRuntimeSettings(runningConfiguration) && 
server.isRunning()) {
+            return;
         }
+        if (Objects.nonNull(runningConfiguration)) {
+            server.shutdown();
+            runningConfiguration = null;
+        }
+        configuration.afterPropertiesSet();
+        server.init();
+        server.start();
+        runningConfiguration = configuration;
     }
 
     @Override
     public String pluginNamed() {
         return PluginEnum.MQTT.getName();
     }
-
-    /**
-     * Ture is use else false.
-     *
-     * @param port server port
-     * @return boolean
-     */
-    private boolean isPortUsing(final int port) {
-        boolean flag = false;
-        try (Socket socket = new Socket(InetAddress.getByName("127.0.0.1"), 
port)) {
-            flag = true;
-        } catch (Exception ignored) {
-
-        }
-        return flag;
-    }
 }
diff --git 
a/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-mqtt/src/test/java/org/apache/shenyu/plugin/mqtt/handler/MqttPluginDataHandlerTest.java
 
b/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-mqtt/src/test/java/org/apache/shenyu/plugin/mqtt/handler/MqttPluginDataHandlerTest.java
index 1e66b443ed..bc827bb356 100644
--- 
a/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-mqtt/src/test/java/org/apache/shenyu/plugin/mqtt/handler/MqttPluginDataHandlerTest.java
+++ 
b/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-mqtt/src/test/java/org/apache/shenyu/plugin/mqtt/handler/MqttPluginDataHandlerTest.java
@@ -17,16 +17,46 @@
 
 package org.apache.shenyu.plugin.mqtt.handler;
 
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.EventLoopGroup;
+import io.netty.util.ResourceLeakDetector;
 import org.apache.shenyu.common.dto.PluginData;
-import org.awaitility.Awaitility;
+import org.apache.shenyu.protocol.mqtt.MqttBootstrapServer;
+import org.apache.shenyu.protocol.mqtt.MqttContext;
+import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
 
-import java.net.InetAddress;
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.lang.management.ManagementFactory;
+import java.lang.management.ThreadInfo;
+import java.lang.reflect.Field;
+import java.net.BindException;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
 import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.concurrent.Callable;
+import java.util.concurrent.FutureTask;
 import java.util.concurrent.TimeUnit;
+import java.util.stream.Stream;
 
+import static org.awaitility.Awaitility.await;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+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;
 
 /**
@@ -34,56 +64,266 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
  */
 public class MqttPluginDataHandlerTest {
 
+    private static final String CONFIG = 
"{\"port\":0,\"workerGroupThreadCount\":1}";
+
     private MqttPluginDataHandler mqttPluginDataHandlerUnderTest;
 
+    private MqttBootstrapServer server;
+
     @BeforeEach
     public void setUp() {
         mqttPluginDataHandlerUnderTest = new MqttPluginDataHandler();
+        server = getField(mqttPluginDataHandlerUnderTest, "server", 
MqttBootstrapServer.class);
+        new MqttContext().setBossGroupThreadCount(1);
+    }
+
+    @AfterEach
+    public void tearDown() {
+        server.shutdown();
+        new MqttContext().setBossGroupThreadCount(0);
+        ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.DISABLED);
+    }
+
+    @Test
+    public void testEnableAndDisableWithoutConfig() {
+        mqttPluginDataHandlerUnderTest.handlerPlugin(pluginData(true, CONFIG));
+        final ChannelFuture future = getField(server, "future", 
ChannelFuture.class);
+        final EventLoopGroup bossGroup = getField(server, "bossGroup", 
EventLoopGroup.class);
+        final EventLoopGroup workerGroup = getField(server, "workerGroup", 
EventLoopGroup.class);
+        assertTrue(future.channel().isActive());
+
+        mqttPluginDataHandlerUnderTest.handlerPlugin(pluginData(false, null));
+
+        assertFalse(future.channel().isActive());
+        assertTrue(bossGroup.isTerminated());
+        assertTrue(workerGroup.isTerminated());
+        assertNull(getField(server, "future", ChannelFuture.class));
+        assertDoesNotThrow(() -> 
mqttPluginDataHandlerUnderTest.handlerPlugin(pluginData(false, null)));
+    }
+
+    @Test
+    public void testEquivalentEnabledUpdatesKeepResources() {
+        mqttPluginDataHandlerUnderTest.handlerPlugin(pluginData(true, CONFIG));
+        final ChannelFuture future = getField(server, "future", 
ChannelFuture.class);
+        final EventLoopGroup bossGroup = getField(server, "bossGroup", 
EventLoopGroup.class);
+        final EventLoopGroup workerGroup = getField(server, "workerGroup", 
EventLoopGroup.class);
+
+        mqttPluginDataHandlerUnderTest.handlerPlugin(pluginData(true, CONFIG));
+        // Defaults, JSON order, level case and the unused boss setting do not 
change the effective configuration.
+        mqttPluginDataHandlerUnderTest.handlerPlugin(pluginData(true, 
"{\"leakDetectorLevel\":\"disabled\",\"password\":\"shenyu\","
+                + 
"\"userName\":\"shenyu\",\"maxPayloadSize\":65536,\"bossGroupThreadCount\":2,\"workerGroupThreadCount\":1,\"port\":0}"));
+
+        assertSame(future, getField(server, "future", ChannelFuture.class));
+        assertSame(bossGroup, getField(server, "bossGroup", 
EventLoopGroup.class));
+        assertSame(workerGroup, getField(server, "workerGroup", 
EventLoopGroup.class));
+        assertTrue(future.channel().isActive());
+    }
+
+    @ParameterizedTest
+    @MethodSource("configurationChanges")
+    public void testChangedConfigurationReplacesResources(final String 
setting, final String value) throws Exception {
+        JsonObject initialConfig = 
JsonParser.parseString(CONFIG).getAsJsonObject();
+        initialConfig.addProperty("port", unusedPort());
+        mqttPluginDataHandlerUnderTest.handlerPlugin(pluginData(true, 
initialConfig.toString()));
+        final ChannelFuture oldFuture = getField(server, "future", 
ChannelFuture.class);
+        final EventLoopGroup oldBoss = getField(server, "bossGroup", 
EventLoopGroup.class);
+        final EventLoopGroup oldWorker = getField(server, "workerGroup", 
EventLoopGroup.class);
+        String changedValue = "port".equals(setting) ? 
Integer.toString(unusedPort()) : value;
+        JsonObject changedConfig = initialConfig.deepCopy();
+        changedConfig.add(setting, JsonParser.parseString(changedValue));
+
+        mqttPluginDataHandlerUnderTest.handlerPlugin(pluginData(true, 
changedConfig.toString()));
+
+        assertFalse(oldFuture.channel().isActive());
+        assertTrue(oldBoss.isTerminated());
+        assertTrue(oldWorker.isTerminated());
+        final ChannelFuture replacement = getField(server, "future", 
ChannelFuture.class);
+        assertNotSame(oldFuture, replacement);
+        assertTrue(replacement.channel().isActive());
+    }
+
+    @Test
+    public void testEncryptedPasswordUpdatesKeepResources() {
+        String encryptedConfig = 
"{\"port\":0,\"workerGroupThreadCount\":1,\"password\":\"secret\",\"isEncryptPassword\":true,\"encryptMode\":\"MD5\"}";
+        mqttPluginDataHandlerUnderTest.handlerPlugin(pluginData(true, 
encryptedConfig));
+        final ChannelFuture future = getField(server, "future", 
ChannelFuture.class);
+        String effectivePassword = new MqttContext().getPassword();
+
+        mqttPluginDataHandlerUnderTest.handlerPlugin(pluginData(true, 
encryptedConfig));
+        mqttPluginDataHandlerUnderTest.handlerPlugin(pluginData(true, 
"{\"port\":0,\"workerGroupThreadCount\":1,\"password\":\"" + effectivePassword 
+ "\"}"));
+
+        assertSame(future, getField(server, "future", ChannelFuture.class));
+        assertTrue(MqttContext.isValid("shenyu", 
effectivePassword.getBytes(StandardCharsets.UTF_8)));
+    }
+
+    @Test
+    public void testChangedPortOnDisableClosesRunningListener() throws 
Exception {
+        mqttPluginDataHandlerUnderTest.handlerPlugin(pluginData(true, CONFIG));
+        final ChannelFuture future = getField(server, "future", 
ChannelFuture.class);
+
+        mqttPluginDataHandlerUnderTest.handlerPlugin(pluginData(false, 
"{\"port\":" + unusedPort() + "}"));
+
+        assertFalse(future.channel().isActive());
+        assertNull(getField(server, "future", ChannelFuture.class));
+    }
+
+    @Test
+    public void testFailedReplacementCanRetryPreviousConfiguration() throws 
Exception {
+        mqttPluginDataHandlerUnderTest.handlerPlugin(pluginData(true, CONFIG));
+        final ChannelFuture previous = getField(server, "future", 
ChannelFuture.class);
+        try (ServerSocket occupied = new ServerSocket(0)) {
+            String failedConfig = "{\"port\":" + occupied.getLocalPort() + 
",\"workerGroupThreadCount\":1}";
+            assertThrows(BindException.class, () -> 
mqttPluginDataHandlerUnderTest.handlerPlugin(pluginData(true, failedConfig)));
+
+            assertFalse(previous.channel().isActive());
+            assertNull(getField(server, "future", ChannelFuture.class));
+        }
+
+        mqttPluginDataHandlerUnderTest.handlerPlugin(pluginData(true, CONFIG));
+
+        assertTrue(getField(server, "future", 
ChannelFuture.class).channel().isActive());
     }
 
     @Test
-    public void testEnableConfiguration() throws InterruptedException {
-        final PluginData enablePluginData = new PluginData("pluginId", 
"pluginName", "{\n"
-                + "  \"port\": 9500,"
-                + "  \"bossGroupThreadCount\": 1,"
-                + "  \"maxPayloadSize\": 65536,"
-                + "  \"workerGroupThreadCount\": 12,"
-                + "  \"userName\": \"shenyu\","
-                + "  \"password\": \"shenyu\","
-                + "  \"isEncryptPassword\": false,"
-                + "  \"encryptMode\": \"\","
-                + "  \"leakDetectorLevel\": \"DISABLED\""
-                + "}", "0", true, null);
-        mqttPluginDataHandlerUnderTest.handlerPlugin(enablePluginData);
-        assertTrue(isPortUsing());
-        final PluginData disablePluginData = new PluginData("pluginId", 
"pluginName", "{\n"
-                + "  \"port\": 9500,"
-                + "  \"bossGroupThreadCount\": 1,"
-                + "  \"maxPayloadSize\": 65536,"
-                + "  \"workerGroupThreadCount\": 12,"
-                + "  \"userName\": \"shenyu\","
-                + "  \"password\": \"shenyu\","
-                + "  \"isEncryptPassword\": false,"
-                + "  \"encryptMode\": \"\","
-                + "  \"leakDetectorLevel\": \"DISABLED\""
-                + "}", "0", false, null);
-        mqttPluginDataHandlerUnderTest.handlerPlugin(disablePluginData);
-
-        Awaitility.await()
-                .atMost(5, TimeUnit.SECONDS)
-                .untilAsserted(() -> assertFalse(isPortUsing()));
-    }
-
-    private boolean isPortUsing() {
-        boolean flag = false;
+    public void testFailedStartCanRetryTheSameConfiguration() throws Exception 
{
+        String config;
+        try (ServerSocket occupied = new ServerSocket(0)) {
+            config = "{\"port\":" + occupied.getLocalPort() + 
",\"workerGroupThreadCount\":1}";
+            assertThrows(BindException.class, () -> 
mqttPluginDataHandlerUnderTest.handlerPlugin(pluginData(true, config)));
+        }
+
+        mqttPluginDataHandlerUnderTest.handlerPlugin(pluginData(true, config));
+
+        assertTrue(getField(server, "future", 
ChannelFuture.class).channel().isActive());
+    }
+
+    @Test
+    public void testDisableThenEnableTheSameConfiguration() {
+        assertDoesNotThrow(() -> 
mqttPluginDataHandlerUnderTest.handlerPlugin(pluginData(false, CONFIG)));
+        mqttPluginDataHandlerUnderTest.handlerPlugin(pluginData(true, CONFIG));
+        final ChannelFuture previous = getField(server, "future", 
ChannelFuture.class);
+        mqttPluginDataHandlerUnderTest.handlerPlugin(pluginData(false, 
CONFIG));
+
+        mqttPluginDataHandlerUnderTest.handlerPlugin(pluginData(true, CONFIG));
+
+        final ChannelFuture current = getField(server, "future", 
ChannelFuture.class);
+        assertNotSame(previous, current);
+        assertTrue(current.channel().isActive());
+    }
+
+    @Test
+    public void testRepeatedUpdatePreservesConnectionAndDisableClosesIt() 
throws Exception {
+        mqttPluginDataHandlerUnderTest.handlerPlugin(pluginData(true, CONFIG));
+        final ChannelFuture future = getField(server, "future", 
ChannelFuture.class);
+        int port = ((InetSocketAddress) 
future.channel().localAddress()).getPort();
+        try (Socket client = new Socket("127.0.0.1", port)) {
+            client.setSoTimeout(5000);
+            ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+            DataOutputStream connect = new DataOutputStream(bytes);
+            connect.writeUTF("MQTT");
+            connect.writeByte(4);
+            connect.writeByte(0xC2);
+            connect.writeShort(60);
+            connect.writeUTF("lifecycle-test-client");
+            connect.writeUTF("shenyu");
+            connect.writeUTF("shenyu");
+            client.getOutputStream().write(0x10);
+            client.getOutputStream().write(bytes.size());
+            client.getOutputStream().write(bytes.toByteArray());
+            assertArrayEquals(new byte[]{0x20, 2, 0, 0}, 
client.getInputStream().readNBytes(4));
+
+            mqttPluginDataHandlerUnderTest.handlerPlugin(pluginData(true, 
CONFIG));
+            client.getOutputStream().write(new byte[]{(byte) 0xC0, 0});
+            assertArrayEquals(new byte[]{(byte) 0xD0, 0}, 
client.getInputStream().readNBytes(2));
+
+            mqttPluginDataHandlerUnderTest.handlerPlugin(pluginData(false, 
CONFIG));
+
+            assertEquals(-1, client.getInputStream().read());
+        }
+    }
+
+    @Test
+    public void testConcurrentEnabledUpdatesStartOnlyOnce() throws Exception {
+        Callable<ChannelFuture> update = () -> {
+            mqttPluginDataHandlerUnderTest.handlerPlugin(pluginData(true, 
CONFIG));
+            return getField(server, "future", ChannelFuture.class);
+        };
+        final FutureTask<ChannelFuture> first = new FutureTask<>(update);
+        final FutureTask<ChannelFuture> second = new FutureTask<>(update);
+        Thread firstThread = new Thread(first);
+        Thread secondThread = new Thread(second);
         try {
-            InetAddress address = InetAddress.getByName("127.0.0.1");
-            Socket socket = new Socket(address, 9500);
-            flag = true;
-        } catch (Exception ignored) {
+            // Pause the first update at the real server's startup monitor.
+            synchronized (server) {
+                firstThread.start();
+                awaitBlockedBy(firstThread, Thread.currentThread());
+                secondThread.start();
+                awaitBlockedBy(secondThread, firstThread);
+            }
 
+            assertSame(first.get(10, TimeUnit.SECONDS), second.get(10, 
TimeUnit.SECONDS));
+            assertTrue(server.isRunning());
+            final EventLoopGroup bossGroup = getField(server, "bossGroup", 
EventLoopGroup.class);
+            final EventLoopGroup workerGroup = getField(server, "workerGroup", 
EventLoopGroup.class);
+            mqttPluginDataHandlerUnderTest.handlerPlugin(pluginData(false, 
CONFIG));
+            assertTrue(bossGroup.isTerminated());
+            assertTrue(workerGroup.isTerminated());
+        } finally {
+            firstThread.interrupt();
+            secondThread.interrupt();
+            firstThread.join(TimeUnit.SECONDS.toMillis(10));
+            secondThread.join(TimeUnit.SECONDS.toMillis(10));
+            assertFalse(firstThread.isAlive());
+            assertFalse(secondThread.isAlive());
         }
-        return flag;
+    }
+
+    @Test
+    public void testSameConfigurationRestartsClosedListener() {
+        mqttPluginDataHandlerUnderTest.handlerPlugin(pluginData(true, CONFIG));
+        final ChannelFuture previous = getField(server, "future", 
ChannelFuture.class);
+        final EventLoopGroup oldBoss = getField(server, "bossGroup", 
EventLoopGroup.class);
+        final EventLoopGroup oldWorker = getField(server, "workerGroup", 
EventLoopGroup.class);
+        previous.channel().close().syncUninterruptibly();
+        assertFalse(server.isRunning());
 
+        mqttPluginDataHandlerUnderTest.handlerPlugin(pluginData(true, CONFIG));
+
+        assertTrue(server.isRunning());
+        assertNotSame(previous, getField(server, "future", 
ChannelFuture.class));
+        assertTrue(oldBoss.isTerminated());
+        assertTrue(oldWorker.isTerminated());
+    }
+
+    private void awaitBlockedBy(final Thread thread, final Thread owner) {
+        await().atMost(Duration.ofSeconds(5)).until(() -> {
+            ThreadInfo info = 
ManagementFactory.getThreadMXBean().getThreadInfo(thread.getId());
+            return info.getThreadState() == Thread.State.BLOCKED && 
info.getLockOwnerId() == owner.getId();
+        });
+    }
+
+    private static Stream<Arguments> configurationChanges() {
+        return Stream.of(Arguments.of("port", "0"), Arguments.of("userName", 
"\"changed\""), Arguments.of("password", "\"changed\""),
+                Arguments.of("maxPayloadSize", "1024"), 
Arguments.of("workerGroupThreadCount", "2"), Arguments.of("leakDetectorLevel", 
"\"SIMPLE\""));
+    }
+
+    private PluginData pluginData(final boolean enabled, final String config) {
+        return new PluginData("pluginId", "mqtt", config, "0", enabled, null);
+    }
+
+    private int unusedPort() throws Exception {
+        try (ServerSocket socket = new ServerSocket(0)) {
+            return socket.getLocalPort();
+        }
+    }
+
+    private <T> T getField(final Object target, final String name, final 
Class<T> type) {
+        try {
+            Field field = target instanceof MqttBootstrapServer ? 
MqttBootstrapServer.class.getDeclaredField(name) : 
MqttPluginDataHandler.class.getDeclaredField(name);
+            field.setAccessible(true);
+            return type.cast(field.get(target));
+        } catch (ReflectiveOperationException e) {
+            throw new AssertionError(e);
+        }
     }
 }
diff --git 
a/shenyu-protocol/shenyu-protocol-mqtt/src/main/java/org/apache/shenyu/protocol/mqtt/MqttBootstrapServer.java
 
b/shenyu-protocol/shenyu-protocol-mqtt/src/main/java/org/apache/shenyu/protocol/mqtt/MqttBootstrapServer.java
index 3e21c24a20..fe417dee92 100644
--- 
a/shenyu-protocol/shenyu-protocol-mqtt/src/main/java/org/apache/shenyu/protocol/mqtt/MqttBootstrapServer.java
+++ 
b/shenyu-protocol/shenyu-protocol-mqtt/src/main/java/org/apache/shenyu/protocol/mqtt/MqttBootstrapServer.java
@@ -54,36 +54,58 @@ public class MqttBootstrapServer implements BootstrapServer 
{
     }
 
     @Override
-    public void start() {
-        //// todo thread start mqtt server
-        
ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.valueOf(ENV.getLeakDetectorLevel().toUpperCase(Locale.ROOT)));
-        bossGroup = new NioEventLoopGroup(ENV.getBossGroupThreadCount());
-        workerGroup = new NioEventLoopGroup(ENV.getWorkerGroupThreadCount());
-        ServerBootstrap bootstrap = new ServerBootstrap();
-        bootstrap.group(bossGroup, workerGroup)
-                .channel(NioServerSocketChannel.class)
-                .childHandler(new 
MqttTransportServerInitializer(ENV.getMaxPayloadSize()));
+    public synchronized void start() {
+        if (isRunning()) {
+            return;
+        }
+        shutdown();
+        boolean started = false;
         try {
-            future = bootstrap.bind(ENV.getPort()).sync();
-            //// todo log
+            
ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.valueOf(ENV.getLeakDetectorLevel().toUpperCase(Locale.ROOT)));
+            bossGroup = new NioEventLoopGroup(ENV.getBossGroupThreadCount());
+            workerGroup = new 
NioEventLoopGroup(ENV.getWorkerGroupThreadCount());
+            ServerBootstrap bootstrap = new ServerBootstrap();
+            bootstrap.group(bossGroup, workerGroup)
+                    .channel(NioServerSocketChannel.class)
+                    .childHandler(new 
MqttTransportServerInitializer(ENV.getMaxPayloadSize()));
+            future = bootstrap.bind(ENV.getPort());
+            future.sync();
+            started = true;
         } catch (InterruptedException e) {
-            //// todo log
+            Thread.currentThread().interrupt();
+            throw new IllegalStateException("Interrupted while starting MQTT 
server", e);
+        } finally {
+            if (!started) {
+                shutdown();
+            }
         }
     }
 
     @Override
-    public void shutdown() {
+    public synchronized void shutdown() {
         if (Objects.nonNull(future)) {
             future.channel().close().syncUninterruptibly();
+            future = null;
         }
         if (Objects.nonNull(bossGroup)) {
             bossGroup.shutdownGracefully().syncUninterruptibly();
+            bossGroup = null;
         }
         if (Objects.nonNull(workerGroup)) {
             workerGroup.shutdownGracefully().syncUninterruptibly();
+            workerGroup = null;
         }
     }
 
+    /**
+     * Whether the MQTT listener is active.
+     *
+     * @return true when the listener is active
+     */
+    public synchronized boolean isRunning() {
+        return Objects.nonNull(future) && future.channel().isActive();
+    }
+
     private void initRepositories() throws IllegalAccessException, 
InstantiationException {
         Reflections reflections = new Reflections(REPOSITORY_PACKAGE_NAME);
         for (Class<? extends BaseRepository> clazz : 
reflections.getSubTypesOf(BaseRepository.class)) {
diff --git 
a/shenyu-protocol/shenyu-protocol-mqtt/src/main/java/org/apache/shenyu/protocol/mqtt/MqttServerConfiguration.java
 
b/shenyu-protocol/shenyu-protocol-mqtt/src/main/java/org/apache/shenyu/protocol/mqtt/MqttServerConfiguration.java
index 5b210df88e..47738a1d75 100644
--- 
a/shenyu-protocol/shenyu-protocol-mqtt/src/main/java/org/apache/shenyu/protocol/mqtt/MqttServerConfiguration.java
+++ 
b/shenyu-protocol/shenyu-protocol-mqtt/src/main/java/org/apache/shenyu/protocol/mqtt/MqttServerConfiguration.java
@@ -19,6 +19,9 @@ package org.apache.shenyu.protocol.mqtt;
 
 import org.apache.shenyu.protocol.mqtt.utils.EncryptUtil;
 
+import java.util.Locale;
+import java.util.Objects;
+
 /**
  * mqtt server configuration.
  */
@@ -47,11 +50,8 @@ public class MqttServerConfiguration {
      */
     public void afterPropertiesSet() {
 
-        if (isEncryptPassword()) {
-            setPassword(encryptPassword());
-        }
         MqttContext context = new MqttContext();
-        context.setPassword(getPassword());
+        context.setPassword(effectivePassword());
         context.setPort(getPort());
         context.setMaxPayloadSize(getMaxPayloadSize());
         context.setUserName(getUserName());
@@ -59,8 +59,30 @@ public class MqttServerConfiguration {
         context.setLeakDetectorLevel(getLeakDetectorLevel());
     }
 
-    private String encryptPassword() {
-        return EncryptUtil.choose(getEncryptMode(), getPassword());
+    private String effectivePassword() {
+        return isEncryptPassword() ? EncryptUtil.choose(getEncryptMode(), 
getPassword()) : getPassword();
+    }
+
+    /**
+     * Compare the settings applied to the MQTT context.
+     *
+     * @param other the configuration to compare
+     * @return true when both configurations have the same runtime settings
+     */
+    public boolean hasSameRuntimeSettings(final MqttServerConfiguration other) 
{
+        if (Objects.isNull(other)) {
+            return false;
+        }
+        return port == other.port
+                && maxPayloadSize == other.maxPayloadSize
+                && workerGroupThreadCount == other.workerGroupThreadCount
+                && Objects.equals(userName, other.userName)
+                && Objects.equals(effectivePassword(), 
other.effectivePassword())
+                && 
Objects.equals(normalizeLeakDetectorLevel(leakDetectorLevel), 
normalizeLeakDetectorLevel(other.leakDetectorLevel));
+    }
+
+    private static String normalizeLeakDetectorLevel(final String level) {
+        return Objects.isNull(level) ? null : level.toUpperCase(Locale.ROOT);
     }
 
     /**
diff --git 
a/shenyu-protocol/shenyu-protocol-mqtt/src/test/java/org/apache/shenyu/protocol/mqtt/MqttBootstrapServerTest.java
 
b/shenyu-protocol/shenyu-protocol-mqtt/src/test/java/org/apache/shenyu/protocol/mqtt/MqttBootstrapServerTest.java
index 66e941c36e..fe8f1f717c 100644
--- 
a/shenyu-protocol/shenyu-protocol-mqtt/src/test/java/org/apache/shenyu/protocol/mqtt/MqttBootstrapServerTest.java
+++ 
b/shenyu-protocol/shenyu-protocol-mqtt/src/test/java/org/apache/shenyu/protocol/mqtt/MqttBootstrapServerTest.java
@@ -28,11 +28,25 @@ import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
 import java.lang.reflect.Field;
+import java.net.BindException;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
 import java.time.Duration;
+import java.util.concurrent.Callable;
+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 java.util.concurrent.atomic.AtomicReference;
 
 import static org.awaitility.Awaitility.await;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+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;
 
 /**
@@ -40,8 +54,11 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
  */
 public final class MqttBootstrapServerTest {
 
+    private MqttBootstrapServer server;
+
     @BeforeEach
     public void setUp() {
+        server = new MqttBootstrapServer();
         MqttContext context = new MqttContext();
         context.setPort(0);
         context.setBossGroupThreadCount(1);
@@ -54,6 +71,7 @@ public final class MqttBootstrapServerTest {
 
     @AfterEach
     public void tearDown() {
+        server.shutdown();
         MqttContext context = new MqttContext();
         context.setPort(0);
         context.setBossGroupThreadCount(0);
@@ -66,8 +84,6 @@ public final class MqttBootstrapServerTest {
 
     @Test
     public void initShouldRegisterAllRepositories() {
-        MqttBootstrapServer server = new MqttBootstrapServer();
-
         server.init();
 
         assertNotNull(Singleton.INST.get(ChannelRepository.class));
@@ -76,26 +92,126 @@ public final class MqttBootstrapServerTest {
     }
 
     @Test
-    public void startAndShutdownShouldReleaseChannelAndEventLoops() throws 
Exception {
-        MqttBootstrapServer server = new MqttBootstrapServer();
-
+    public void startAndShutdownShouldReleaseChannelAndEventLoops() {
         server.start();
 
-        ChannelFuture future = getField(server, "future", ChannelFuture.class);
+        final ChannelFuture future = getField(server, "future", 
ChannelFuture.class);
+        final EventLoopGroup bossGroup = getField(server, "bossGroup", 
EventLoopGroup.class);
+        final EventLoopGroup workerGroup = getField(server, "workerGroup", 
EventLoopGroup.class);
         assertTrue(future.channel().isActive());
 
         server.shutdown();
 
         assertFalse(future.channel().isActive());
-        EventLoopGroup bossGroup = getField(server, "bossGroup", 
EventLoopGroup.class);
-        EventLoopGroup workerGroup = getField(server, "workerGroup", 
EventLoopGroup.class);
         await().atMost(Duration.ofSeconds(5)).until(bossGroup::isTerminated);
         await().atMost(Duration.ofSeconds(5)).until(workerGroup::isTerminated);
+        assertNull(getField(server, "future", ChannelFuture.class));
+        assertNull(getField(server, "bossGroup", EventLoopGroup.class));
+        assertNull(getField(server, "workerGroup", EventLoopGroup.class));
+    }
+
+    @Test
+    public void repeatedStartShouldKeepTheSameResources() {
+        server.start();
+        final ChannelFuture future = getField(server, "future", 
ChannelFuture.class);
+        final EventLoopGroup bossGroup = getField(server, "bossGroup", 
EventLoopGroup.class);
+        final EventLoopGroup workerGroup = getField(server, "workerGroup", 
EventLoopGroup.class);
+
+        // Reuse the bound port so a second bind would fail instead of 
choosing another ephemeral port.
+        new MqttContext().setPort(((InetSocketAddress) 
future.channel().localAddress()).getPort());
+        server.start();
+
+        assertSame(future, getField(server, "future", ChannelFuture.class));
+        assertSame(bossGroup, getField(server, "bossGroup", 
EventLoopGroup.class));
+        assertSame(workerGroup, getField(server, "workerGroup", 
EventLoopGroup.class));
+        assertTrue(future.channel().isActive());
+    }
+
+    @Test
+    public void failedBindShouldReleaseEventLoopsAndAllowRetry() throws 
Exception {
+        final AtomicReference<EventLoopGroup> allocatedBoss = new 
AtomicReference<>();
+        final AtomicReference<EventLoopGroup> allocatedWorker = new 
AtomicReference<>();
+        server = new MqttBootstrapServer() {
+            @Override
+            public void shutdown() {
+                allocatedBoss.set(getField(this, "bossGroup", 
EventLoopGroup.class));
+                allocatedWorker.set(getField(this, "workerGroup", 
EventLoopGroup.class));
+                super.shutdown();
+            }
+        };
+        try (ServerSocket occupied = new ServerSocket(0)) {
+            new MqttContext().setPort(occupied.getLocalPort());
+
+            assertThrows(BindException.class, server::start);
+
+            assertTrue(allocatedBoss.get().isTerminated());
+            assertTrue(allocatedWorker.get().isTerminated());
+            assertNull(getField(server, "future", ChannelFuture.class));
+            assertNull(getField(server, "bossGroup", EventLoopGroup.class));
+            assertNull(getField(server, "workerGroup", EventLoopGroup.class));
+        }
+
+        new MqttContext().setPort(0);
+        server.start();
+        assertTrue(getField(server, "future", 
ChannelFuture.class).channel().isActive());
+    }
+
+    @Test
+    public void concurrentStartsShouldKeepTheSameResources() throws Exception {
+        final CountDownLatch ready = new CountDownLatch(2);
+        final CountDownLatch start = new CountDownLatch(1);
+        Callable<ChannelFuture> task = () -> {
+            ready.countDown();
+            assertTrue(start.await(5, TimeUnit.SECONDS));
+            server.start();
+            return getField(server, "future", ChannelFuture.class);
+        };
+        ExecutorService executor = Executors.newFixedThreadPool(2);
+        try {
+            final Future<ChannelFuture> first = executor.submit(task);
+            final Future<ChannelFuture> second = executor.submit(task);
+            assertTrue(ready.await(5, TimeUnit.SECONDS));
+            start.countDown();
+
+            assertSame(first.get(10, TimeUnit.SECONDS), second.get(10, 
TimeUnit.SECONDS));
+            assertTrue(server.isRunning());
+            final EventLoopGroup bossGroup = getField(server, "bossGroup", 
EventLoopGroup.class);
+            final EventLoopGroup workerGroup = getField(server, "workerGroup", 
EventLoopGroup.class);
+            server.shutdown();
+            assertTrue(bossGroup.isTerminated());
+            assertTrue(workerGroup.isTerminated());
+        } finally {
+            start.countDown();
+            executor.shutdownNow();
+            assertTrue(executor.awaitTermination(10, TimeUnit.SECONDS));
+        }
+    }
+
+    @Test
+    public void startShouldReplaceClosedListenerAndReleaseOldEventLoops() {
+        server.start();
+        final ChannelFuture previous = getField(server, "future", 
ChannelFuture.class);
+        final EventLoopGroup oldBoss = getField(server, "bossGroup", 
EventLoopGroup.class);
+        final EventLoopGroup oldWorker = getField(server, "workerGroup", 
EventLoopGroup.class);
+        new MqttContext().setPort(((InetSocketAddress) 
previous.channel().localAddress()).getPort());
+        previous.channel().close().syncUninterruptibly();
+        assertFalse(server.isRunning());
+
+        server.start();
+
+        assertTrue(server.isRunning());
+        assertNotSame(previous, getField(server, "future", 
ChannelFuture.class));
+        assertTrue(oldBoss.isTerminated());
+        assertTrue(oldWorker.isTerminated());
     }
 
-    private <T> T getField(final Object target, final String name, final 
Class<T> type) throws Exception {
-        Field field = MqttBootstrapServer.class.getDeclaredField(name);
-        field.setAccessible(true);
-        return type.cast(field.get(target));
+    private <T> T getField(final Object target, final String name, final 
Class<T> type) {
+        try {
+            Field field = MqttBootstrapServer.class.getDeclaredField(name);
+            field.setAccessible(true);
+            return type.cast(field.get(target));
+        } catch (ReflectiveOperationException e) {
+            throw new AssertionError(e);
+        }
     }
 }

Reply via email to