Aias00 commented on code in PR #7157:
URL: https://github.com/apache/shenyu/pull/7157#discussion_r4068604841


##########
shenyu-infra/shenyu-infra-redis/src/main/java/org/apache/shenyu/infra/redis/RedisConnectionFactory.java:
##########
@@ -56,6 +62,34 @@ public LettuceConnectionFactory 
getLettuceConnectionFactory() {
         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() {

Review Comment:
   Adding the lifecycle end here is the right move: this wrapper is the only 
type that owns the two things `LettuceConnectionFactory` created but nobody 
released - the connection pool and the per-client Netty resources - so 
`destroy()` belongs to whoever created them.
   
   I specifically checked the failure mode that would have made this dangerous: 
could releasing one Shenyu redis client take down another plugin's client 
because they share Lettuce `ClientResources`? It cannot, and the reasoning is 
worth writing down because it is load-bearing for every call site below:
   
   1. `getLettuceClientConfiguration(...)` builds 
`LettucePoolingClientConfiguration.builder().poolConfig(...).build()` and never 
calls `.clientResources(...)`, so the configuration carries no resources.
   2. Lettuce 6.3.2, `AbstractRedisClient` constructor (decompiled from 
`lettuce-core-6.3.2.RELEASE.jar`): `if (clientResources == null) { 
sharedResources = false; clientResources = DefaultClientResources.create(); }` 
- each client creates and therefore owns its own resources.
   3. `AbstractRedisClient.closeClientResources(...)` only does the full 
`clientResources.shutdown(...)` when `sharedResources == false`; otherwise it 
just releases the event loop groups it borrowed.
   
   So `destroy()` releases exactly one client's own resources and nothing else. 
Two further properties make it safe to call from the handlers: 
`LettuceConnectionFactory.stop()` only does work under 
`state.compareAndSet(STARTED, STOPPING)`, so a second `destroy()` is a no-op, 
and `isRunning()` is a clean observable to assert on.
   
   One note rather than a request: the class is not a Spring bean anywhere, and 
`RedisConnectionFactory` is not a `ReactiveRedisConnectionFactory` either, so 
`destroyQuietly` can never actually be handed one - every call site passes the 
inner `LettuceConnectionFactory`, which has always implemented `DisposableBean` 
on its own. Right now the instance `destroy()` has exactly one caller, its own 
unit test. It is still the right home for the lifecycle, but if you want it to 
earn its keep, the follow-up would be to have the call sites register 
themselves here instead of extracting the lettuce factory.



##########
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 void handlerPlugin(final PluginData pluginData) {
             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);

Review Comment:
   Since `Singleton.INST` is keyed on `ReactiveRedisTemplate.class`, it is 
worth being explicit about what this entry is: a process-wide slot, not 
per-plugin state. I went looking for a second writer and there isn't one - 
`RateLimiterPluginDataHandler` is the only place that calls 
`Singleton.INST.single(ReactiveRedisTemplate.class, ...)`; `RedisRateLimiter` 
and `ConcurrentRateLimiterAlgorithm` only read it. So destroying what was there 
before is safe: whatever it was, this handler put it there.
   
   Two smaller observations:
   
   - Caching `previousRedisTemplate` *before* the new one is installed is 
correct. What it does not close is the gap between a request reading 
`Singleton.INST.get(ReactiveRedisTemplate.class)` and the moment it subscribes 
- a config push landing inside that window now fails the request where 
previously it would have succeeded (the old client stayed usable). That is the 
tradeoff documented in the PR description, and I agree it is worth paying to 
stop leaking a pool per config change; just calling it out so it is a recorded 
decision rather than an accident. If you want to narrow it later, the fix 
belongs in a follow-up.
   - Note the `||` chain above means an entry can exist with 
`RedisConfigProperties.class` absent, so rebuild also covers the legacy 
half-initialised state - good, and the new destroy handles it too.



##########
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 void close() {
             connection.close();
         } catch (Exception ignored) {
         }
+        // the factory owns the connection pool and its threads, closing a 
connection does not release them
+        RedisConnectionFactory.destroyQuietly(connectionFactory);

Review Comment:
   This is the right hook to release on. Worth being aware of how the caller 
sequences it, because this one is looser than the other three sites in this PR:
   
   `CachePluginDataHandler`:
   ```java
   this.closeCacheIfNeed();                                            // 
destroys the old cache here
   final ICacheBuilder cacheBuilder = ExtensionLoader...getJoin(...);
   Singleton.INST.single(ICache.class, cacheBuilder.builderCache(config));  // 
new one installed later
   ```
   
   The old hand - `closeCacheIfNeed()` - only does this:
   ```java
   ICache lastCache = CacheUtils.getCache();       // 
Singleton.INST.get(ICache.class)
   ...
   lastCache.close();
   ```
   It never removes `ICache.class` from `Singleton.INST`. So between the 
destroy above and the install below, `CacheUtils.getCache()` still hands out 
the destroyed cache, and after `removePlugin` it stays there indefinitely.
   
   Before this change that window was survivable: `close()` only returned two 
borrowed connections to the pool, so a request that had already grabbed the 
cache still worked. Now the factory underneath it is destroyed, so that same 
request fails. Same class of window the other handlers have, but here it can be 
closed cheaply:
   
   - either install first, then close the previous one (what you did for the 
other three handlers), or
   - have `closeCacheIfNeed()` evict `ICache.class` right after 
`lastCache.close()`.
   
   Also happy to leave it as a follow-up issue if you prefer to keep this PR 
scoped to the leak - just say so and I will drop it. Flagging it because the 
cache plugin is the one replacement path where the old instance stays reachable 
after being destroyed.
   
   Detail, no action needed: with pooling enabled, 
`connectionFactory.getReactiveConnection()` borrows a pooled connection and 
`close()` returns it, so the two lines above behave as intended rather than 
opening throwaway sockets.
   
   Note on verification: I could not execute this one. `RedisCacheTest` is 
compiled and declares three `@Test` methods, but Surefire reports `Tests run: 
0` for it in this module (targeted and untargeted runs alike) on my machine - 
same before your change, so it is not something this PR introduced. I checked 
the existing `closeCache()` test by reading: its last `close()` runs against 
mocked factories, which are not `DisposableBean`, so `destroyQuietly` no-ops 
there, and the real cache is not used after `close()`.



##########
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 void handlerPlugin(final PluginData pluginData) {
             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());

Review Comment:
   Installing the new template with `cachedHandle(...)` *before* destroying the 
old one is exactly the right order - new requests can never observe a destroyed 
client. Capturing `cachedTemplate` before mutating the caches is also 
necessary, since after `cachedHandle` the handle is already the new template. 
Same thing in `removePlugin` just below, where destroying before `removeHandle` 
keeps the DTO reachable for the release.
   
   Two things you may want to fold in later, neither blocking:
   
   1. With pooling configured (`LettucePoolingClientConfiguration`), what was 
actually being leaked here per config change was the pool plus one owned set of 
Netty event loops - see the comment on `RedisConnectionFactory#destroy` for why 
releasing it does not disturb the other plugins' clients. Would be nice to have 
a note here saying the released client must not be reused, e.g. by using it 
from a `finally` that swaps the reference.
   
   2. `removePlugin` now releases the client, which this handler did not do 
before - good. For symmetry, `AiTokenLimiterPluginHandler` and 
`RateLimiterPluginDataHandler` still have no `removePlugin`: those clients stay 
alive (with their pool and threads) for the rest of the process lifetime after 
the plugin is removed, because nothing ever clears their `CommonHandleCache` / 
`Singleton` entries. Not a regression introduced here, but it is the same bug 
through a different door, and now there is a `destroyQuietly` helper that makes 
the follow-up a two-line change per handler.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to