Copilot commented on code in PR #15891:
URL: https://github.com/apache/dubbo/pull/15891#discussion_r2638978810


##########
dubbo-registry/dubbo-registry-nacos/src/main/java/org/apache/dubbo/registry/nacos/util/NacosNamingServiceUtils.java:
##########
@@ -104,22 +125,103 @@ public static String getGroup(URL connectionURL) {
         return connectionURL.getParameter(NACOS_GROUP_KEY, group);
     }
 
+    // This ensures that different registry groups sharing the same Nacos 
server and namespace reuse a single
+    // physical connection.
+    private static String createNamingServiceCacheKey(URL connectionURL) {
+        URL normalized = normalizeConnectionURL(connectionURL);
+        return normalized.toFullString();
+    }
+
     /**
-     * Create an instance of {@link NamingService} from specified {@link URL 
connection url}
+     * Create or obtain a shared {@link NacosNamingServiceWrapper} for the 
given connection URL.
      *
-     * @param connectionURL {@link URL connection url}
-     * @return {@link NamingService}
+     * @param connectionURL the registry connection URL
+     * @return a shared {@link NacosNamingServiceWrapper}
      * @since 2.7.5
      */
     public static NacosNamingServiceWrapper createNamingService(URL 
connectionURL) {
+        String key = createNamingServiceCacheKey(connectionURL);
+
+        // Create or retrieve the shared service holder.
+        NacosNamingServiceHolder holder = SERVICE_CACHE.compute(key, (k, v) -> 
{
+            if (v == null) {
+                logger.info("Creating shared NacosNamingService for key: {}", 
key);
+                NacosNamingServiceWrapper newWrapper = 
createWrapperInternal(connectionURL);
+                v = new NacosNamingServiceHolder(newWrapper);
+            }
+            v.refCount.incrementAndGet();
+            return v;
+        });
+
+        return holder.wrapper;
+    }
+
+    // Release a previously acquired naming service reference.
+    public static void releaseNamingService(URL connectionURL) {
+        String key = createNamingServiceCacheKey(connectionURL);
+
+        // Decrement the reference count and close the connection if it 
reaches zero.
+        SERVICE_CACHE.compute(key, (k, v) -> {
+            if (v == null) {
+                return null;
+            }
+
+            if (v.refCount.decrementAndGet() <= 0) {

Review Comment:
   The condition `v.refCount.decrementAndGet() <= 0` has a race condition risk. 
If the count becomes negative due to unbalanced release calls (more releases 
than creates), the wrapper will be shut down and removed from the cache, but 
subsequent releases will find `v == null` and silently return. This could mask 
bugs where releaseNamingService is called more times than createNamingService 
for the same URL.
   
   Consider changing the condition to check `== 0` instead of `<= 0`, and add 
explicit logging or an assertion when the count goes negative to help detect 
misuse.
   ```suggestion
               int newRefCount = v.refCount.decrementAndGet();
               if (newRefCount < 0) {
                   logger.warn(REGISTRY_NACOS_EXCEPTION, "", "",
                       "NacosNamingService reference count for key " + key + " 
dropped below zero: " + newRefCount);
               }
               if (newRefCount == 0) {
   ```



##########
dubbo-registry/dubbo-registry-nacos/src/main/java/org/apache/dubbo/registry/nacos/util/NacosNamingServiceUtils.java:
##########
@@ -104,22 +125,103 @@ public static String getGroup(URL connectionURL) {
         return connectionURL.getParameter(NACOS_GROUP_KEY, group);
     }
 
+    // This ensures that different registry groups sharing the same Nacos 
server and namespace reuse a single
+    // physical connection.
+    private static String createNamingServiceCacheKey(URL connectionURL) {
+        URL normalized = normalizeConnectionURL(connectionURL);
+        return normalized.toFullString();
+    }
+
     /**
-     * Create an instance of {@link NamingService} from specified {@link URL 
connection url}
+     * Create or obtain a shared {@link NacosNamingServiceWrapper} for the 
given connection URL.
      *
-     * @param connectionURL {@link URL connection url}
-     * @return {@link NamingService}
+     * @param connectionURL the registry connection URL
+     * @return a shared {@link NacosNamingServiceWrapper}
      * @since 2.7.5
      */
     public static NacosNamingServiceWrapper createNamingService(URL 
connectionURL) {
+        String key = createNamingServiceCacheKey(connectionURL);
+
+        // Create or retrieve the shared service holder.
+        NacosNamingServiceHolder holder = SERVICE_CACHE.compute(key, (k, v) -> 
{
+            if (v == null) {
+                logger.info("Creating shared NacosNamingService for key: {}", 
key);
+                NacosNamingServiceWrapper newWrapper = 
createWrapperInternal(connectionURL);
+                v = new NacosNamingServiceHolder(newWrapper);
+            }
+            v.refCount.incrementAndGet();
+            return v;
+        });
+
+        return holder.wrapper;
+    }
+
+    // Release a previously acquired naming service reference.

Review Comment:
   The new public method `releaseNamingService` lacks proper JavaDoc 
documentation. While the internal comment on line 159 explains its purpose, 
users of this API need to understand:
   1. When and why to call this method
   2. That it must be called exactly once for each corresponding 
createNamingService() call
   3. What happens if it's called without a prior createNamingService() call
   4. Thread-safety guarantees
   
   Consider adding comprehensive JavaDoc similar to the style used for 
createNamingService().
   ```suggestion
       /**
        * Release a previously acquired shared {@link 
NacosNamingServiceWrapper} for the given connection URL.
        * <p>
        * This method must be called exactly once for every successful 
invocation of
        * {@link #createNamingService(URL)} using an equivalent {@link URL} 
(i.e., one that results in the same
        * internal cache key). When the reference count for the corresponding 
cached entry drops to zero, the
        * underlying {@link NamingService} is shut down and removed from the 
cache.
        * </p>
        * <p>
        * If this method is called with a {@code connectionURL} for which no 
{@link #createNamingService(URL)}
        * call has been made before (or if the associated entry has already 
been fully released), the call is a
        * no-op and no exception is thrown.
        * </p>
        * <p>
        * This method is thread-safe. Reference counting and cache updates are 
performed atomically using a
        * {@link ConcurrentMap} and {@link 
java.util.concurrent.atomic.AtomicInteger}, so it is safe to call
        * {@code createNamingService} and {@code releaseNamingService} 
concurrently from multiple threads.
        * </p>
        *
        * @param connectionURL the registry connection URL associated with the 
naming service to release
        * @since 2.7.5
        */
   ```



##########
dubbo-registry/dubbo-registry-nacos/src/main/java/org/apache/dubbo/registry/nacos/util/NacosNamingServiceUtils.java:
##########
@@ -104,22 +125,103 @@ public static String getGroup(URL connectionURL) {
         return connectionURL.getParameter(NACOS_GROUP_KEY, group);
     }
 
+    // This ensures that different registry groups sharing the same Nacos 
server and namespace reuse a single
+    // physical connection.
+    private static String createNamingServiceCacheKey(URL connectionURL) {
+        URL normalized = normalizeConnectionURL(connectionURL);
+        return normalized.toFullString();
+    }
+
     /**
-     * Create an instance of {@link NamingService} from specified {@link URL 
connection url}
+     * Create or obtain a shared {@link NacosNamingServiceWrapper} for the 
given connection URL.
      *
-     * @param connectionURL {@link URL connection url}
-     * @return {@link NamingService}
+     * @param connectionURL the registry connection URL
+     * @return a shared {@link NacosNamingServiceWrapper}
      * @since 2.7.5
      */
     public static NacosNamingServiceWrapper createNamingService(URL 
connectionURL) {
+        String key = createNamingServiceCacheKey(connectionURL);
+
+        // Create or retrieve the shared service holder.
+        NacosNamingServiceHolder holder = SERVICE_CACHE.compute(key, (k, v) -> 
{
+            if (v == null) {
+                logger.info("Creating shared NacosNamingService for key: {}", 
key);
+                NacosNamingServiceWrapper newWrapper = 
createWrapperInternal(connectionURL);
+                v = new NacosNamingServiceHolder(newWrapper);
+            }
+            v.refCount.incrementAndGet();
+            return v;
+        });

Review Comment:
   There's a subtle race condition in the reference counting logic. If two 
threads call createNamingService() concurrently with the same key, both could 
observe `v == null`, create separate NacosNamingServiceHolder instances, and 
one would overwrite the other. While ConcurrentHashMap.compute() is atomic, the 
logic inside creates a new holder on line 150 and then increments its refCount 
on line 152. If the holder gets replaced between creation and incrementing, the 
reference count will be incorrect.
   
   The current implementation is actually safe because compute() guarantees 
atomicity of the entire lambda execution, but for clarity and robustness, 
consider initializing the refCount to 1 in the constructor instead of 0, and 
only increment it for existing holders. This makes the intent clearer and 
removes the possibility of future bugs if the code is refactored.



##########
dubbo-registry/dubbo-registry-nacos/src/main/java/org/apache/dubbo/registry/nacos/util/NacosNamingServiceUtils.java:
##########
@@ -104,22 +125,103 @@ public static String getGroup(URL connectionURL) {
         return connectionURL.getParameter(NACOS_GROUP_KEY, group);
     }
 
+    // This ensures that different registry groups sharing the same Nacos 
server and namespace reuse a single
+    // physical connection.
+    private static String createNamingServiceCacheKey(URL connectionURL) {
+        URL normalized = normalizeConnectionURL(connectionURL);
+        return normalized.toFullString();
+    }
+
     /**
-     * Create an instance of {@link NamingService} from specified {@link URL 
connection url}
+     * Create or obtain a shared {@link NacosNamingServiceWrapper} for the 
given connection URL.
      *
-     * @param connectionURL {@link URL connection url}
-     * @return {@link NamingService}
+     * @param connectionURL the registry connection URL
+     * @return a shared {@link NacosNamingServiceWrapper}
      * @since 2.7.5
      */
     public static NacosNamingServiceWrapper createNamingService(URL 
connectionURL) {
+        String key = createNamingServiceCacheKey(connectionURL);
+
+        // Create or retrieve the shared service holder.
+        NacosNamingServiceHolder holder = SERVICE_CACHE.compute(key, (k, v) -> 
{
+            if (v == null) {
+                logger.info("Creating shared NacosNamingService for key: {}", 
key);
+                NacosNamingServiceWrapper newWrapper = 
createWrapperInternal(connectionURL);
+                v = new NacosNamingServiceHolder(newWrapper);
+            }
+            v.refCount.incrementAndGet();
+            return v;
+        });
+
+        return holder.wrapper;
+    }
+
+    // Release a previously acquired naming service reference.
+    public static void releaseNamingService(URL connectionURL) {
+        String key = createNamingServiceCacheKey(connectionURL);
+
+        // Decrement the reference count and close the connection if it 
reaches zero.
+        SERVICE_CACHE.compute(key, (k, v) -> {
+            if (v == null) {
+                return null;
+            }
+
+            if (v.refCount.decrementAndGet() <= 0) {
+                try {
+                    logger.info("Destroying shared NacosNamingService for key: 
{}", key);
+                    v.wrapper.shutdown();
+                } catch (Exception e) {
+                    logger.warn(
+                            REGISTRY_NACOS_EXCEPTION, "", "", "Failed to 
destroy naming service for key: " + key, e);
+                }
+                return null;
+            }
+            return v;
+        });
+    }
+
+    /**
+     * Create a new {@link NacosNamingServiceWrapper} using the normalized
+     * connection URL and configured retry options.
+     */
+    private static NacosNamingServiceWrapper createWrapperInternal(URL 
connectionURL) {
+        URL normalized = normalizeConnectionURL(connectionURL);
+
         boolean check = connectionURL.getParameter(NACOS_CHECK_KEY, true);
         int retryTimes = connectionURL.getPositiveParameter(NACOS_RETRY_KEY, 
10);
         int sleepMsBetweenRetries = 
connectionURL.getPositiveParameter(NACOS_RETRY_WAIT_KEY, 10);
+
         if (check && !UrlUtils.isCheck(connectionURL)) {
             check = false;
         }
+
         NacosConnectionManager nacosConnectionManager =
-                new NacosConnectionManager(connectionURL, check, retryTimes, 
sleepMsBetweenRetries);
+                new NacosConnectionManager(normalized, check, retryTimes, 
sleepMsBetweenRetries);
         return new NacosNamingServiceWrapper(nacosConnectionManager, 
retryTimes, sleepMsBetweenRetries);
     }
+
+    /**
+     * Normalizes the URL by removing group parameters and sorting the server 
address list.
+     * This method removes grouping parameters and standardizes the server 
address list

Review Comment:
   The javadoc comment is incomplete - it ends abruptly without explaining what 
the method does with the standardized server address list. Complete the 
documentation to explain that addresses are trimmed, deduplicated (via sorted 
stream), and sorted alphabetically to ensure consistent cache keys regardless 
of the order addresses are specified in the URL.
   ```suggestion
        * This method removes grouping parameters and standardizes the server 
address list by trimming
        * whitespace, discarding empty entries, deduplicating and sorting 
addresses alphabetically so
        * that the resulting URL produces consistent cache keys regardless of 
the order in which
        * server addresses are specified.
   ```



##########
dubbo-registry/dubbo-registry-nacos/src/main/java/org/apache/dubbo/registry/nacos/util/NacosNamingServiceUtils.java:
##########
@@ -104,22 +125,103 @@ public static String getGroup(URL connectionURL) {
         return connectionURL.getParameter(NACOS_GROUP_KEY, group);
     }
 
+    // This ensures that different registry groups sharing the same Nacos 
server and namespace reuse a single
+    // physical connection.
+    private static String createNamingServiceCacheKey(URL connectionURL) {
+        URL normalized = normalizeConnectionURL(connectionURL);
+        return normalized.toFullString();
+    }
+
     /**
-     * Create an instance of {@link NamingService} from specified {@link URL 
connection url}
+     * Create or obtain a shared {@link NacosNamingServiceWrapper} for the 
given connection URL.
      *
-     * @param connectionURL {@link URL connection url}
-     * @return {@link NamingService}
+     * @param connectionURL the registry connection URL
+     * @return a shared {@link NacosNamingServiceWrapper}
      * @since 2.7.5
      */
     public static NacosNamingServiceWrapper createNamingService(URL 
connectionURL) {
+        String key = createNamingServiceCacheKey(connectionURL);
+
+        // Create or retrieve the shared service holder.
+        NacosNamingServiceHolder holder = SERVICE_CACHE.compute(key, (k, v) -> 
{
+            if (v == null) {
+                logger.info("Creating shared NacosNamingService for key: {}", 
key);
+                NacosNamingServiceWrapper newWrapper = 
createWrapperInternal(connectionURL);
+                v = new NacosNamingServiceHolder(newWrapper);
+            }
+            v.refCount.incrementAndGet();
+            return v;
+        });
+
+        return holder.wrapper;
+    }
+
+    // Release a previously acquired naming service reference.
+    public static void releaseNamingService(URL connectionURL) {
+        String key = createNamingServiceCacheKey(connectionURL);
+
+        // Decrement the reference count and close the connection if it 
reaches zero.
+        SERVICE_CACHE.compute(key, (k, v) -> {
+            if (v == null) {
+                return null;
+            }
+
+            if (v.refCount.decrementAndGet() <= 0) {
+                try {
+                    logger.info("Destroying shared NacosNamingService for key: 
{}", key);
+                    v.wrapper.shutdown();
+                } catch (Exception e) {
+                    logger.warn(
+                            REGISTRY_NACOS_EXCEPTION, "", "", "Failed to 
destroy naming service for key: " + key, e);
+                }
+                return null;
+            }
+            return v;
+        });
+    }
+
+    /**
+     * Create a new {@link NacosNamingServiceWrapper} using the normalized
+     * connection URL and configured retry options.
+     */
+    private static NacosNamingServiceWrapper createWrapperInternal(URL 
connectionURL) {
+        URL normalized = normalizeConnectionURL(connectionURL);
+
         boolean check = connectionURL.getParameter(NACOS_CHECK_KEY, true);
         int retryTimes = connectionURL.getPositiveParameter(NACOS_RETRY_KEY, 
10);
         int sleepMsBetweenRetries = 
connectionURL.getPositiveParameter(NACOS_RETRY_WAIT_KEY, 10);
+
         if (check && !UrlUtils.isCheck(connectionURL)) {
             check = false;
         }
+
         NacosConnectionManager nacosConnectionManager =
-                new NacosConnectionManager(connectionURL, check, retryTimes, 
sleepMsBetweenRetries);
+                new NacosConnectionManager(normalized, check, retryTimes, 
sleepMsBetweenRetries);
         return new NacosNamingServiceWrapper(nacosConnectionManager, 
retryTimes, sleepMsBetweenRetries);

Review Comment:
   The normalization logic recreates the URL using 
`URL.valueOf(connectionURL.toServiceStringWithoutResolving())`, which may not 
preserve all parameters from the original URL. Parameters like `nacos.retry`, 
`nacos.retry-wait`, and `nacos.check` are needed in createWrapperInternal() 
(lines 190-192) but are retrieved from the original `connectionURL` parameter, 
not from `normalized`.
   
   While this works correctly because createWrapperInternal() receives the 
original connectionURL as a parameter, it creates a subtle inconsistency: the 
normalized URL is used for creating the NacosConnectionManager, but retry 
parameters come from the original URL. This could be confusing for future 
maintainers. Consider adding a comment explaining why some parameters are taken 
from the original URL rather than the normalized one.



##########
dubbo-registry/dubbo-registry-nacos/src/test/java/org/apache/dubbo/registry/nacos/util/NacosNamingServiceUtilsTest.java:
##########
@@ -172,4 +175,9 @@ public String getServerStatus() {
             }
         }
     }
+
+    @AfterEach
+    void tearDown() {
+        NacosNamingServiceUtils.clearCacheForTest();
+    }
 }

Review Comment:
   The test suite is missing coverage for the core connection reuse 
functionality introduced in this PR. There are no tests that verify:
   1. Multiple createNamingService() calls with the same connection parameters 
return the same wrapper instance
   2. The reference counting mechanism works correctly (incrementing on create, 
decrementing on release)
   3. The connection is only shut down when the last reference is released
   4. Connections are properly reused across different registry groups
   
   Consider adding a test that creates multiple NamingService instances with 
different group parameters but the same server address, verifies they share the 
same underlying connection, then releases them and confirms the connection is 
only closed after all references are released.



##########
dubbo-registry/dubbo-registry-nacos/src/main/java/org/apache/dubbo/registry/nacos/util/NacosNamingServiceUtils.java:
##########
@@ -104,22 +125,103 @@ public static String getGroup(URL connectionURL) {
         return connectionURL.getParameter(NACOS_GROUP_KEY, group);
     }
 
+    // This ensures that different registry groups sharing the same Nacos 
server and namespace reuse a single
+    // physical connection.
+    private static String createNamingServiceCacheKey(URL connectionURL) {
+        URL normalized = normalizeConnectionURL(connectionURL);
+        return normalized.toFullString();
+    }
+
     /**
-     * Create an instance of {@link NamingService} from specified {@link URL 
connection url}
+     * Create or obtain a shared {@link NacosNamingServiceWrapper} for the 
given connection URL.
      *
-     * @param connectionURL {@link URL connection url}
-     * @return {@link NamingService}
+     * @param connectionURL the registry connection URL
+     * @return a shared {@link NacosNamingServiceWrapper}
      * @since 2.7.5
      */
     public static NacosNamingServiceWrapper createNamingService(URL 
connectionURL) {
+        String key = createNamingServiceCacheKey(connectionURL);
+
+        // Create or retrieve the shared service holder.
+        NacosNamingServiceHolder holder = SERVICE_CACHE.compute(key, (k, v) -> 
{
+            if (v == null) {
+                logger.info("Creating shared NacosNamingService for key: {}", 
key);
+                NacosNamingServiceWrapper newWrapper = 
createWrapperInternal(connectionURL);
+                v = new NacosNamingServiceHolder(newWrapper);
+            }
+            v.refCount.incrementAndGet();
+            return v;
+        });
+
+        return holder.wrapper;
+    }
+
+    // Release a previously acquired naming service reference.
+    public static void releaseNamingService(URL connectionURL) {
+        String key = createNamingServiceCacheKey(connectionURL);
+
+        // Decrement the reference count and close the connection if it 
reaches zero.
+        SERVICE_CACHE.compute(key, (k, v) -> {
+            if (v == null) {
+                return null;
+            }
+
+            if (v.refCount.decrementAndGet() <= 0) {
+                try {
+                    logger.info("Destroying shared NacosNamingService for key: 
{}", key);
+                    v.wrapper.shutdown();
+                } catch (Exception e) {
+                    logger.warn(
+                            REGISTRY_NACOS_EXCEPTION, "", "", "Failed to 
destroy naming service for key: " + key, e);
+                }
+                return null;
+            }
+            return v;
+        });
+    }
+
+    /**
+     * Create a new {@link NacosNamingServiceWrapper} using the normalized
+     * connection URL and configured retry options.
+     */
+    private static NacosNamingServiceWrapper createWrapperInternal(URL 
connectionURL) {
+        URL normalized = normalizeConnectionURL(connectionURL);
+
         boolean check = connectionURL.getParameter(NACOS_CHECK_KEY, true);
         int retryTimes = connectionURL.getPositiveParameter(NACOS_RETRY_KEY, 
10);
         int sleepMsBetweenRetries = 
connectionURL.getPositiveParameter(NACOS_RETRY_WAIT_KEY, 10);
+
         if (check && !UrlUtils.isCheck(connectionURL)) {
             check = false;
         }
+
         NacosConnectionManager nacosConnectionManager =
-                new NacosConnectionManager(connectionURL, check, retryTimes, 
sleepMsBetweenRetries);
+                new NacosConnectionManager(normalized, check, retryTimes, 
sleepMsBetweenRetries);
         return new NacosNamingServiceWrapper(nacosConnectionManager, 
retryTimes, sleepMsBetweenRetries);
     }
+
+    /**
+     * Normalizes the URL by removing group parameters and sorting the server 
address list.
+     * This method removes grouping parameters and standardizes the server 
address list
+     */
+    private static URL normalizeConnectionURL(URL connectionURL) {
+        URL normalized = 
URL.valueOf(connectionURL.toServiceStringWithoutResolving())
+                .removeParameter(GROUP_KEY)
+                .removeParameter(NACOS_GROUP_KEY);
+
+        String serverAddr = normalized.getParameter("serverAddr", 
normalized.getAddress());
+        if (serverAddr != null) {
+            String canonical = Arrays.stream(serverAddr.split(","))
+                    .map(String::trim)
+                    .filter(s -> !s.isEmpty())
+                    .sorted()
+                    .collect(Collectors.joining(","));
+            normalized = normalized.addParameter("serverAddr", canonical);
+        }

Review Comment:
   The fallback `normalized.getAddress()` on line 212 may not provide a 
meaningful value when `serverAddr` parameter is present in the URL. In Nacos 
configurations, the server address is typically specified via the `serverAddr` 
parameter (comma-separated list), not via the URL host/port. If `serverAddr` is 
null or empty, falling back to a single host from the URL may not accurately 
represent the connection identity.
   
   Consider whether the normalization should fail or log a warning when neither 
`serverAddr` parameter nor a valid address is present, as this could indicate a 
misconfiguration.
   ```suggestion
           // Prefer explicitly configured serverAddr; only fall back to URL 
address if non-empty.
           String serverAddr = normalized.getParameter("serverAddr");
           if (serverAddr == null || serverAddr.trim().isEmpty()) {
               String addressFromUrl = normalized.getAddress();
               if (addressFromUrl != null && !addressFromUrl.trim().isEmpty()) {
                   serverAddr = addressFromUrl;
               } else {
                   // Neither serverAddr nor a valid URL address is present; 
this likely indicates misconfiguration.
                   logger.warn(REGISTRY_NACOS_EXCEPTION, "", "",
                           "No valid Nacos server address found in URL 
parameters or host/port: " + connectionURL);
                   return normalized;
               }
           }
   
           String canonical = Arrays.stream(serverAddr.split(","))
                   .map(String::trim)
                   .filter(s -> !s.isEmpty())
                   .sorted()
                   .collect(Collectors.joining(","));
           normalized = normalized.addParameter("serverAddr", canonical);
   ```



-- 
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]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to