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 1a8ff4257b fix: release the redis client that is replaced (#7156) 
(#7157)
1a8ff4257b is described below

commit 1a8ff4257b12e4d9794c23182d8c43f9f7d10ef2
Author: HY-love-sleep <[email protected]>
AuthorDate: Tue Sep 22 13:46:27 2026 +0800

    fix: release the redis client that is replaced (#7156) (#7157)
---
 .../shenyu/infra/redis/RedisConnectionFactory.java |  36 +++++++-
 .../infra/redis/RedisConnectionFactoryTest.java    |  41 +++++++++
 .../handler/SensitiveWordPluginDataHandler.java    |  11 ++-
 .../SensitiveWordPluginDataHandlerTest.java        |  54 +++++++++++
 .../handler/AiTokenLimiterPluginHandler.java       |   6 ++
 .../handler/AiTokenLimiterPluginHandlerTest.java   | 102 +++++++++++++++++++++
 .../shenyu/plugin/cache/redis/RedisCache.java      |   2 +
 .../handler/RateLimiterPluginDataHandler.java      |   5 +
 .../handler/RateLimiterPluginDataHandlerTest.java  |  29 ++++++
 9 files changed, 284 insertions(+), 2 deletions(-)

diff --git 
a/shenyu-infra/shenyu-infra-redis/src/main/java/org/apache/shenyu/infra/redis/RedisConnectionFactory.java
 
b/shenyu-infra/shenyu-infra-redis/src/main/java/org/apache/shenyu/infra/redis/RedisConnectionFactory.java
index f8f68bb269..90f666434e 100644
--- 
a/shenyu-infra/shenyu-infra-redis/src/main/java/org/apache/shenyu/infra/redis/RedisConnectionFactory.java
+++ 
b/shenyu-infra/shenyu-infra-redis/src/main/java/org/apache/shenyu/infra/redis/RedisConnectionFactory.java
@@ -21,6 +21,10 @@ import com.google.common.base.Splitter;
 import com.google.common.collect.Lists;
 import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
 import org.apache.shenyu.common.enums.RedisModeEnum;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.DisposableBean;
+import 
org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
 import org.springframework.data.redis.connection.RedisClusterConfiguration;
 import org.springframework.data.redis.connection.RedisNode;
 import org.springframework.data.redis.connection.RedisPassword;
@@ -38,7 +42,9 @@ import java.util.Objects;
 /**
  * RedisConnectionFactory.
  */
-public class RedisConnectionFactory {
+public class RedisConnectionFactory implements DisposableBean {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(RedisConnectionFactory.class);
 
     private final LettuceConnectionFactory lettuceConnectionFactory;
 
@@ -56,6 +62,34 @@ public class RedisConnectionFactory {
         return this.lettuceConnectionFactory;
     }
 
+    /**
+     * Destroy the lettuce connection factory and the connection pool it owns. 
The client this factory
+     * was built for must not be used afterwards.
+     */
+    @Override
+    public void destroy() {
+        lettuceConnectionFactory.destroy();
+    }
+
+    /**
+     * Destroy a connection factory that was created by this class or by 
{@link #getLettuceConnectionFactory()}.
+     * The handlers of the plugins that rebuild their client on a 
configuration change keep the reactive
+     * template only, so this is how they release the client they replace. A 
null factory, or one that has
+     * no lifecycle, is ignored; a failure is logged rather than thrown, 
because a client that cannot be
+     * released must not break the configuration update that replaces it.
+     *
+     * @param connectionFactory the connection factory to destroy, may be null
+     */
+    public static void destroyQuietly(final ReactiveRedisConnectionFactory 
connectionFactory) {
+        if (connectionFactory instanceof DisposableBean) {
+            try {
+                ((DisposableBean) connectionFactory).destroy();
+            } catch (Exception e) {
+                LOG.warn("failed to destroy the redis connection factory", e);
+            }
+        }
+    }
+
     private LettuceConnectionFactory createLettuceConnectionFactory(final 
RedisConfigProperties redisConfigProperties) {
         LettuceClientConfiguration lettuceClientConfiguration = 
getLettuceClientConfiguration(redisConfigProperties);
         if 
(RedisModeEnum.SENTINEL.getName().equals(redisConfigProperties.getMode())) {
diff --git 
a/shenyu-infra/shenyu-infra-redis/src/test/java/org/apache/shenyu/infra/redis/RedisConnectionFactoryTest.java
 
b/shenyu-infra/shenyu-infra-redis/src/test/java/org/apache/shenyu/infra/redis/RedisConnectionFactoryTest.java
index 0f3f3cbe8d..fe18982ed8 100644
--- 
a/shenyu-infra/shenyu-infra-redis/src/test/java/org/apache/shenyu/infra/redis/RedisConnectionFactoryTest.java
+++ 
b/shenyu-infra/shenyu-infra-redis/src/test/java/org/apache/shenyu/infra/redis/RedisConnectionFactoryTest.java
@@ -21,7 +21,10 @@ import org.apache.shenyu.common.enums.RedisModeEnum;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 import org.mockito.Mockito;
+import org.springframework.beans.factory.DisposableBean;
+import 
org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
 import org.springframework.data.redis.connection.RedisNode;
+import 
org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
 
 import java.time.Duration;
 import java.lang.reflect.Method;
@@ -53,6 +56,38 @@ public class RedisConnectionFactoryTest {
         Assertions.assertDoesNotThrow(() -> new 
RedisConnectionFactory(redisConfigProperties));
     }
 
+    @Test
+    public void destroyDestroysTheLettuceFactory() {
+        RedisConfigProperties redisConfigProperties = new 
RedisConfigProperties();
+        redisConfigProperties.setUrl("localhost:6379");
+        redisConfigProperties.setMode(RedisModeEnum.STANDALONE.getName());
+        RedisConnectionFactory factory = new 
RedisConnectionFactory(redisConfigProperties);
+        LettuceConnectionFactory lettuceConnectionFactory = 
factory.getLettuceConnectionFactory();
+        Assertions.assertTrue(lettuceConnectionFactory.isRunning());
+        factory.destroy();
+        Assertions.assertFalse(lettuceConnectionFactory.isRunning());
+    }
+
+    @Test
+    public void destroyQuietlyDestroysTheFactory() throws Exception {
+        DisposableReactiveFactory connectionFactory = 
Mockito.mock(DisposableReactiveFactory.class);
+        RedisConnectionFactory.destroyQuietly(connectionFactory);
+        Mockito.verify(connectionFactory).destroy();
+    }
+
+    @Test
+    public void destroyQuietlyIgnoresWhatItCannotDestroy() throws Exception {
+        // nothing to destroy
+        Assertions.assertDoesNotThrow(() -> 
RedisConnectionFactory.destroyQuietly(null));
+        // a factory of another type, without a lifecycle, is left alone
+        Assertions.assertDoesNotThrow(() -> 
RedisConnectionFactory.destroyQuietly(
+                Mockito.mock(ReactiveRedisConnectionFactory.class)));
+        // a failure is logged instead of thrown: replacing a client must not 
fail because of it
+        DisposableReactiveFactory failing = 
Mockito.mock(DisposableReactiveFactory.class);
+        Mockito.doThrow(new 
IllegalStateException("boom")).when(failing).destroy();
+        Assertions.assertDoesNotThrow(() -> 
RedisConnectionFactory.destroyQuietly(failing));
+    }
+
     @Test
     public void parseRedisNodeValidInputs() throws Exception {
         RedisConnectionFactory factory = createFactoryWithDefaultUrl();
@@ -118,4 +153,10 @@ public class RedisConnectionFactoryTest {
             }
         });
     }
+
+    /**
+     * A reactive factory that has a lifecycle, which is what the lettuce 
factory is in production.
+     */
+    private interface DisposableReactiveFactory extends 
ReactiveRedisConnectionFactory, DisposableBean {
+    }
 }
diff --git 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-sensitive-word/src/main/java/org/apache/shenyu/plugin/ai/sensitive/word/handler/SensitiveWordPluginDataHandler.java
 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-sensitive-word/src/main/java/org/apache/shenyu/plugin/ai/sensitive/word/handler/SensitiveWordPluginDataHandler.java
index 699f83367c..f155162434 100644
--- 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-sensitive-word/src/main/java/org/apache/shenyu/plugin/ai/sensitive/word/handler/SensitiveWordPluginDataHandler.java
+++ 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-sensitive-word/src/main/java/org/apache/shenyu/plugin/ai/sensitive/word/handler/SensitiveWordPluginDataHandler.java
@@ -89,19 +89,28 @@ public class SensitiveWordPluginDataHandler implements 
PluginDataHandler {
             return;
         }
         RedisConfigProperties cachedProperties = 
REDIS_PROPERTIES.get().obtainHandle(PLUGIN_NAME);
-        if (Objects.isNull(REDIS_TEMPLATES.get().obtainHandle(PLUGIN_NAME)) || 
!redisConfig.equals(cachedProperties)) {
+        ReactiveRedisTemplate<String, String> cachedTemplate = 
REDIS_TEMPLATES.get().obtainHandle(PLUGIN_NAME);
+        if (Objects.isNull(cachedTemplate) || 
!redisConfig.equals(cachedProperties)) {
             RedisConnectionFactory connectionFactory = new 
RedisConnectionFactory(redisConfig);
             ReactiveRedisTemplate<String, String> redisTemplate = new 
ShenyuReactiveRedisTemplate<>(
                     connectionFactory.getLettuceConnectionFactory(),
                     
ShenyuRedisSerializationContext.stringSerializationContext());
             REDIS_TEMPLATES.get().cachedHandle(PLUGIN_NAME, redisTemplate);
             REDIS_PROPERTIES.get().cachedHandle(PLUGIN_NAME, redisConfig);
+            // the client that is replaced must not keep its connection pool 
and its threads alive
+            if (Objects.nonNull(cachedTemplate)) {
+                
RedisConnectionFactory.destroyQuietly(cachedTemplate.getConnectionFactory());
+            }
             LOG.info("sensitive word plugin: cached the reactive redis 
template");
         }
     }
 
     @Override
     public void removePlugin(final PluginData pluginData) {
+        ReactiveRedisTemplate<String, String> cachedTemplate = 
REDIS_TEMPLATES.get().obtainHandle(PLUGIN_NAME);
+        if (Objects.nonNull(cachedTemplate)) {
+            
RedisConnectionFactory.destroyQuietly(cachedTemplate.getConnectionFactory());
+        }
         REDIS_TEMPLATES.get().removeHandle(PLUGIN_NAME);
         REDIS_PROPERTIES.get().removeHandle(PLUGIN_NAME);
         LOG.info("sensitive word plugin: released the cached redis template");
diff --git 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-sensitive-word/src/test/java/org/apache/shenyu/plugin/ai/sensitive/word/handler/SensitiveWordPluginDataHandlerTest.java
 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-sensitive-word/src/test/java/org/apache/shenyu/plugin/ai/sensitive/word/handler/SensitiveWordPluginDataHandlerTest.java
index 767da72c7d..10b2f66987 100644
--- 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-sensitive-word/src/test/java/org/apache/shenyu/plugin/ai/sensitive/word/handler/SensitiveWordPluginDataHandlerTest.java
+++ 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-sensitive-word/src/test/java/org/apache/shenyu/plugin/ai/sensitive/word/handler/SensitiveWordPluginDataHandlerTest.java
@@ -27,10 +27,15 @@ import 
org.apache.shenyu.plugin.ai.sensitive.word.handler.SensitiveWordPluginDat
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import 
org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
+import org.springframework.data.redis.core.ReactiveRedisTemplate;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+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.assertTrue;
 
 /**
@@ -146,6 +151,44 @@ public final class SensitiveWordPluginDataHandlerTest {
                 .obtainHandle(SensitiveWordPluginDataHandler.PLUGIN_NAME));
     }
 
+    @Test
+    public void testHandlerPluginDestroysTheClientItReplaces() {
+        handler.handlerPlugin(pluginData("127.0.0.1:6379"));
+        ReactiveRedisTemplate<String, String> first = 
SensitiveWordPluginDataHandler.REDIS_TEMPLATES.get()
+                .obtainHandle(SensitiveWordPluginDataHandler.PLUGIN_NAME);
+        assertNotNull(first);
+        assertTrue(lettuceFactory(first).isRunning());
+
+        handler.handlerPlugin(pluginData("127.0.0.1:6380"));
+        ReactiveRedisTemplate<String, String> second = 
SensitiveWordPluginDataHandler.REDIS_TEMPLATES.get()
+                .obtainHandle(SensitiveWordPluginDataHandler.PLUGIN_NAME);
+        assertNotSame(first, second);
+        // the client that was replaced must not keep its connection pool and 
its threads alive
+        assertFalse(lettuceFactory(first).isRunning());
+        assertTrue(lettuceFactory(second).isRunning());
+    }
+
+    @Test
+    public void 
testHandlerPluginKeepsTheClientWhenTheConfigurationIsUnchanged() {
+        handler.handlerPlugin(pluginData("127.0.0.1:6379"));
+        ReactiveRedisTemplate<String, String> first = 
SensitiveWordPluginDataHandler.REDIS_TEMPLATES.get()
+                .obtainHandle(SensitiveWordPluginDataHandler.PLUGIN_NAME);
+        handler.handlerPlugin(pluginData("127.0.0.1:6379"));
+        assertSame(first, SensitiveWordPluginDataHandler.REDIS_TEMPLATES.get()
+                .obtainHandle(SensitiveWordPluginDataHandler.PLUGIN_NAME));
+        assertTrue(lettuceFactory(first).isRunning());
+    }
+
+    @Test
+    public void testRemovePluginDestroysTheClient() {
+        handler.handlerPlugin(pluginData("127.0.0.1:6379"));
+        ReactiveRedisTemplate<String, String> cached = 
SensitiveWordPluginDataHandler.REDIS_TEMPLATES.get()
+                .obtainHandle(SensitiveWordPluginDataHandler.PLUGIN_NAME);
+        assertNotNull(cached);
+        handler.removePlugin(new PluginData());
+        assertFalse(lettuceFactory(cached).isRunning());
+    }
+
     @Test
     public void testCachedDictionaryExpires() {
         CachedDictionary dictionary = new 
CachedDictionary(AhoCorasick.empty());
@@ -154,6 +197,17 @@ public final class SensitiveWordPluginDataHandlerTest {
         assertTrue(!dictionary.isExpired(300L));
     }
 
+    private PluginData pluginData(final String url) {
+        PluginData pluginData = new PluginData();
+        pluginData.setEnabled(true);
+        pluginData.setConfig("{\"url\":\"" + url + "\"}");
+        return pluginData;
+    }
+
+    private LettuceConnectionFactory lettuceFactory(final 
ReactiveRedisTemplate<String, String> template) {
+        return (LettuceConnectionFactory) template.getConnectionFactory();
+    }
+
     private RuleData ruleData(final String handle) {
         RuleData ruleData = new RuleData();
         ruleData.setId("rule-1");
diff --git 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-token-limiter/src/main/java/org/apache/shenyu/plugin/ai/token/limiter/handler/AiTokenLimiterPluginHandler.java
 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-token-limiter/src/main/java/org/apache/shenyu/plugin/ai/token/limiter/handler/AiTokenLimiterPluginHandler.java
index b97738471e..aba6e52757 100644
--- 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-token-limiter/src/main/java/org/apache/shenyu/plugin/ai/token/limiter/handler/AiTokenLimiterPluginHandler.java
+++ 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-token-limiter/src/main/java/org/apache/shenyu/plugin/ai/token/limiter/handler/AiTokenLimiterPluginHandler.java
@@ -62,12 +62,18 @@ public class AiTokenLimiterPluginHandler implements 
PluginDataHandler {
             if 
(Objects.isNull(REDIS_CACHED_HANDLE.get().obtainHandle(PluginEnum.AI_TOKEN_LIMITER.getName()))
                     || 
Objects.isNull(REDIS_PROPERTIES_CACHED_HANDLE.get().obtainHandle(PluginEnum.AI_TOKEN_LIMITER.getName()))
                     || 
!redisConfigProperties.equals(REDIS_PROPERTIES_CACHED_HANDLE.get().obtainHandle(PluginEnum.AI_TOKEN_LIMITER.getName())))
 {
+                final ReactiveRedisTemplate previousRedisTemplate = 
REDIS_CACHED_HANDLE.get()
+                        .obtainHandle(PluginEnum.AI_TOKEN_LIMITER.getName());
                 final RedisConnectionFactory redisConnectionFactory = new 
RedisConnectionFactory(redisConfigProperties);
                 ReactiveRedisTemplate<String, String> reactiveRedisTemplate = 
new ShenyuReactiveRedisTemplate<>(
                         redisConnectionFactory.getLettuceConnectionFactory(),
                         
ShenyuRedisSerializationContext.stringSerializationContext());
                 
REDIS_CACHED_HANDLE.get().cachedHandle(PluginEnum.AI_TOKEN_LIMITER.getName(), 
reactiveRedisTemplate);
                 
REDIS_PROPERTIES_CACHED_HANDLE.get().cachedHandle(PluginEnum.AI_TOKEN_LIMITER.getName(),
 redisConfigProperties);
+                // The client that is replaced must not keep its connection 
pool and its threads alive.
+                if (Objects.nonNull(previousRedisTemplate)) {
+                    
RedisConnectionFactory.destroyQuietly(previousRedisTemplate.getConnectionFactory());
+                }
             }
         }
     }
diff --git 
a/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-token-limiter/src/test/java/org/apache/shenyu/plugin/ai/token/limiter/handler/AiTokenLimiterPluginHandlerTest.java
 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-token-limiter/src/test/java/org/apache/shenyu/plugin/ai/token/limiter/handler/AiTokenLimiterPluginHandlerTest.java
new file mode 100644
index 0000000000..eb7e3000a7
--- /dev/null
+++ 
b/shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-token-limiter/src/test/java/org/apache/shenyu/plugin/ai/token/limiter/handler/AiTokenLimiterPluginHandlerTest.java
@@ -0,0 +1,102 @@
+/*
+ * 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.ai.token.limiter.handler;
+
+import org.apache.shenyu.common.dto.PluginData;
+import org.apache.shenyu.common.enums.PluginEnum;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import 
org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
+import org.springframework.data.redis.core.ReactiveRedisTemplate;
+
+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.assertTrue;
+
+/**
+ * Test cases for {@link AiTokenLimiterPluginHandler}.
+ */
+public final class AiTokenLimiterPluginHandlerTest {
+
+    @AfterEach
+    public void tearDown() {
+        
AiTokenLimiterPluginHandler.REDIS_CACHED_HANDLE.get().removeHandle(PluginEnum.AI_TOKEN_LIMITER.getName());
+        AiTokenLimiterPluginHandler.REDIS_PROPERTIES_CACHED_HANDLE.get()
+                .removeHandle(PluginEnum.AI_TOKEN_LIMITER.getName());
+    }
+
+    @Test
+    public void testHandlerPluginCachesTheRedisTemplate() {
+        new 
AiTokenLimiterPluginHandler().handlerPlugin(pluginData("127.0.0.1:6379"));
+        ReactiveRedisTemplate<?, ?> template = redisTemplate();
+        assertNotNull(template);
+        assertTrue(lettuceFactory(template).isRunning());
+    }
+
+    @Test
+    public void testHandlerPluginDestroysTheClientItReplaces() {
+        AiTokenLimiterPluginHandler handler = new 
AiTokenLimiterPluginHandler();
+        handler.handlerPlugin(pluginData("127.0.0.1:6379"));
+        ReactiveRedisTemplate<?, ?> first = redisTemplate();
+        assertNotNull(first);
+
+        handler.handlerPlugin(pluginData("127.0.0.1:6380"));
+        ReactiveRedisTemplate<?, ?> second = redisTemplate();
+        assertNotSame(first, second);
+        // the client that was replaced must not keep its connection pool and 
its threads alive
+        assertFalse(lettuceFactory(first).isRunning());
+        assertTrue(lettuceFactory(second).isRunning());
+    }
+
+    @Test
+    public void 
testHandlerPluginKeepsTheClientWhenTheConfigurationIsUnchanged() {
+        AiTokenLimiterPluginHandler handler = new 
AiTokenLimiterPluginHandler();
+        handler.handlerPlugin(pluginData("127.0.0.1:6379"));
+        ReactiveRedisTemplate<?, ?> first = redisTemplate();
+        handler.handlerPlugin(pluginData("127.0.0.1:6379"));
+        assertSame(first, redisTemplate());
+        assertTrue(lettuceFactory(first).isRunning());
+    }
+
+    @Test
+    public void testHandlerPluginDisabledDoesNothing() {
+        PluginData pluginData = pluginData("127.0.0.1:6379");
+        pluginData.setEnabled(false);
+        new AiTokenLimiterPluginHandler().handlerPlugin(pluginData);
+        assertNull(redisTemplate());
+    }
+
+    private ReactiveRedisTemplate<?, ?> redisTemplate() {
+        return AiTokenLimiterPluginHandler.REDIS_CACHED_HANDLE.get()
+                .obtainHandle(PluginEnum.AI_TOKEN_LIMITER.getName());
+    }
+
+    private LettuceConnectionFactory lettuceFactory(final 
ReactiveRedisTemplate<?, ?> template) {
+        return (LettuceConnectionFactory) template.getConnectionFactory();
+    }
+
+    private PluginData pluginData(final String url) {
+        PluginData pluginData = new PluginData();
+        pluginData.setEnabled(true);
+        pluginData.setConfig("{\"url\":\"" + url + "\"}");
+        return pluginData;
+    }
+}
diff --git 
a/shenyu-plugin/shenyu-plugin-cache/shenyu-plugin-cache-redis/src/main/java/org/apache/shenyu/plugin/cache/redis/RedisCache.java
 
b/shenyu-plugin/shenyu-plugin-cache/shenyu-plugin-cache-redis/src/main/java/org/apache/shenyu/plugin/cache/redis/RedisCache.java
index b5e54e2ee6..7e2518a206 100644
--- 
a/shenyu-plugin/shenyu-plugin-cache/shenyu-plugin-cache-redis/src/main/java/org/apache/shenyu/plugin/cache/redis/RedisCache.java
+++ 
b/shenyu-plugin/shenyu-plugin-cache/shenyu-plugin-cache-redis/src/main/java/org/apache/shenyu/plugin/cache/redis/RedisCache.java
@@ -89,5 +89,7 @@ public final class RedisCache implements ICache {
             connection.close();
         } catch (Exception ignored) {
         }
+        // the factory owns the connection pool and its threads, closing a 
connection does not release them
+        RedisConnectionFactory.destroyQuietly(connectionFactory);
     }
 }
diff --git 
a/shenyu-plugin/shenyu-plugin-fault-tolerance/shenyu-plugin-ratelimiter/src/main/java/org/apache/shenyu/plugin/ratelimiter/handler/RateLimiterPluginDataHandler.java
 
b/shenyu-plugin/shenyu-plugin-fault-tolerance/shenyu-plugin-ratelimiter/src/main/java/org/apache/shenyu/plugin/ratelimiter/handler/RateLimiterPluginDataHandler.java
index c0203e2014..246ce6dcee 100644
--- 
a/shenyu-plugin/shenyu-plugin-fault-tolerance/shenyu-plugin-ratelimiter/src/main/java/org/apache/shenyu/plugin/ratelimiter/handler/RateLimiterPluginDataHandler.java
+++ 
b/shenyu-plugin/shenyu-plugin-fault-tolerance/shenyu-plugin-ratelimiter/src/main/java/org/apache/shenyu/plugin/ratelimiter/handler/RateLimiterPluginDataHandler.java
@@ -55,12 +55,17 @@ public class RateLimiterPluginDataHandler implements 
PluginDataHandler {
             if (Objects.isNull(Singleton.INST.get(ReactiveRedisTemplate.class))
                     || 
Objects.isNull(Singleton.INST.get(RedisConfigProperties.class))
                     || 
!redisConfigProperties.equals(Singleton.INST.get(RedisConfigProperties.class))) 
{
+                final ReactiveRedisTemplate previousRedisTemplate = 
Singleton.INST.get(ReactiveRedisTemplate.class);
                 final RedisConnectionFactory redisConnectionFactory = new 
RedisConnectionFactory(redisConfigProperties);
                 ReactiveRedisTemplate<String, String> reactiveRedisTemplate = 
new ShenyuReactiveRedisTemplate<>(
                         redisConnectionFactory.getLettuceConnectionFactory(),
                         
ShenyuRedisSerializationContext.stringSerializationContext());
                 Singleton.INST.single(ReactiveRedisTemplate.class, 
reactiveRedisTemplate);
                 Singleton.INST.single(RedisConfigProperties.class, 
redisConfigProperties);
+                // The client that is replaced must not keep its connection 
pool and its threads alive.
+                if (Objects.nonNull(previousRedisTemplate)) {
+                    
RedisConnectionFactory.destroyQuietly(previousRedisTemplate.getConnectionFactory());
+                }
             }
         }
     }
diff --git 
a/shenyu-plugin/shenyu-plugin-fault-tolerance/shenyu-plugin-ratelimiter/src/test/java/org/apache/shenyu/plugin/ratelimiter/handler/RateLimiterPluginDataHandlerTest.java
 
b/shenyu-plugin/shenyu-plugin-fault-tolerance/shenyu-plugin-ratelimiter/src/test/java/org/apache/shenyu/plugin/ratelimiter/handler/RateLimiterPluginDataHandlerTest.java
index c040496f55..0e46e1a1c7 100644
--- 
a/shenyu-plugin/shenyu-plugin-fault-tolerance/shenyu-plugin-ratelimiter/src/test/java/org/apache/shenyu/plugin/ratelimiter/handler/RateLimiterPluginDataHandlerTest.java
+++ 
b/shenyu-plugin/shenyu-plugin-fault-tolerance/shenyu-plugin-ratelimiter/src/test/java/org/apache/shenyu/plugin/ratelimiter/handler/RateLimiterPluginDataHandlerTest.java
@@ -36,14 +36,18 @@ import org.springframework.data.redis.connection.RedisNode;
 import org.springframework.data.redis.connection.RedisPassword;
 import org.springframework.data.redis.connection.RedisSentinelConfiguration;
 import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
+import 
org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
 import org.springframework.data.redis.core.ReactiveRedisTemplate;
 import org.springframework.test.util.ReflectionTestUtils;
 
 import java.util.Collections;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+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.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 /**
  * RateLimiterPluginDataHandler test.
@@ -91,6 +95,31 @@ public final class RateLimiterPluginDataHandlerTest {
         assertNotNull(Singleton.INST.get(ReactiveRedisTemplate.class));
     }
 
+    /**
+     * the client that is replaced must not keep its connection pool and its 
threads alive.
+     */
+    @Test
+    public void handlerPluginDestroysTheClientItReplaces() {
+        RateLimiterPluginDataHandler handler = new 
RateLimiterPluginDataHandler();
+        
handler.handlerPlugin(pluginData(generateRedisConfig("localhost:6379")));
+        ReactiveRedisTemplate first = 
Singleton.INST.get(ReactiveRedisTemplate.class);
+        assertNotNull(first);
+        assertTrue(((LettuceConnectionFactory) 
first.getConnectionFactory()).isRunning());
+
+        
handler.handlerPlugin(pluginData(generateRedisConfig("localhost:6380")));
+        ReactiveRedisTemplate second = 
Singleton.INST.get(ReactiveRedisTemplate.class);
+        assertNotSame(first, second);
+        assertFalse(((LettuceConnectionFactory) 
first.getConnectionFactory()).isRunning());
+        assertTrue(((LettuceConnectionFactory) 
second.getConnectionFactory()).isRunning());
+    }
+
+    private PluginData pluginData(final RedisConfigProperties 
redisConfigProperties) {
+        PluginData pluginData = new PluginData();
+        pluginData.setEnabled(true);
+        
pluginData.setConfig(GsonUtils.getInstance().toJson(redisConfigProperties));
+        return pluginData;
+    }
+
     /**
      * parts parse result null test case.
      */

Reply via email to