Copilot commented on code in PR #2112:
URL: https://github.com/apache/fluss/pull/2112#discussion_r2642102718


##########
fluss-common/src/main/java/org/apache/fluss/utils/ExceptionUtils.java:
##########
@@ -365,10 +369,65 @@ public static <T extends Throwable> T firstOrSuppressed(T 
newException, @Nullabl
 
         if (previous == null || previous == newException) {
             return newException;
-        } else {
-            previous.addSuppressed(newException);
+        }
+
+        // If the exceptions already reference each other through the 
suppression or cause chains,
+        // return the previous exception to avoid introducing cycles.
+        if (existsInExceptionChain(newException, previous)
+                || existsInExceptionChain(previous, newException)) {
             return previous;
         }
+
+        previous.addSuppressed(newException);
+        return previous;
+    }
+
+    /**
+     * Checks whether the given {@code exception} throwable exception exists 
anywhere within the

Review Comment:
   The javadoc contains redundant wording: "the given {@code exception} 
throwable exception". This should be simplified to either "the given {@code 
exception}" or "the given throwable exception" to avoid redundancy.
   ```suggestion
        * Checks whether the given {@code exception} exists anywhere within the
   ```



##########
fluss-common/src/test/java/org/apache/fluss/utils/ExceptionUtilsTest.java:
##########
@@ -121,6 +122,35 @@ void testFirstOrSuppressed() {
         assertThat(suppressedException.getSuppressed()).isEqualTo(new 
Throwable[] {newException});
     }
 
+    @Test
+    void testFirstOrSuppressedCyclePrevention() {
+        // create two test exceptions (assuming thrown during shutdown, etc.)
+        Exception exceptionA = new Exception("Exception A");
+        Exception exceptionB = new Exception("Exception B");
+
+        // associate the suppressions (creating a suppression chain)
+        ExceptionUtils.firstOrSuppressed(exceptionB, exceptionA);
+        assertThat(exceptionA.getSuppressed()).contains(exceptionB);
+
+        // attempt to create a suppression cycle (A -> B; B -> A)
+        Exception result = ExceptionUtils.firstOrSuppressed(exceptionA, 
exceptionB);
+        assertThat(result).isEqualTo(exceptionB);
+
+        // verify the exception cycle was prevented (no bidirectional 
reference)
+        assertThat(exceptionA.getSuppressed()).contains(exceptionB);
+        assertThat(exceptionB.getSuppressed()).doesNotContain(exceptionA);
+        assertThat(exceptionB.getSuppressed()).isEmpty();
+
+        // verify that processing suppressed exceptions no longer causes 
StackOverflowError
+        Assertions.assertDoesNotThrow(() -> 
recursivelyProcessSuppressedExceptions(exceptionA));
+    }

Review Comment:
   The test `testFirstOrSuppressedCyclePrevention` only validates cycle 
prevention through the suppressed exception chain. However, the 
`existsInExceptionChain` method also traverses the cause chain (line 424-427). 
Consider adding a test case that validates cycle prevention when exceptions are 
linked through cause chains, to ensure comprehensive test coverage of the 
implemented behavior.



##########
fluss-common/src/main/java/org/apache/fluss/utils/ExceptionUtils.java:
##########
@@ -365,10 +369,65 @@ public static <T extends Throwable> T firstOrSuppressed(T 
newException, @Nullabl
 
         if (previous == null || previous == newException) {
             return newException;
-        } else {
-            previous.addSuppressed(newException);
+        }
+
+        // If the exceptions already reference each other through the 
suppression or cause chains,
+        // return the previous exception to avoid introducing cycles.
+        if (existsInExceptionChain(newException, previous)
+                || existsInExceptionChain(previous, newException)) {
             return previous;
         }
+
+        previous.addSuppressed(newException);
+        return previous;
+    }
+
+    /**
+     * Checks whether the given {@code exception} throwable exception exists 
anywhere within the
+     * exception chain of {@code previous}. This includes both the cause chain 
and all suppressed
+     * exceptions. A visited set is used to avoid cycles and redundant 
traversal.
+     *
+     * @param exception The throwable exception to search for.
+     * @param previous The previous throwable exception chain to search in.
+     * @return True, if the exception is found within the suppressed chain, 
false otherwise.
+     */
+    private static boolean existsInExceptionChain(Throwable exception, 
Throwable previous) {
+        if (exception == null || previous == null) {
+            return false;
+        }
+        if (exception == previous) {
+            return true;
+        }
+
+        // Apply cycle prevention through a graph-like traversal of existing
+        // suppressed or cause chain exceptions
+        Set<Throwable> previousExceptions = new HashSet<>();
+        Deque<Throwable> exceptionStack = new ArrayDeque<>();
+        exceptionStack.push(previous);
+
+        while (!exceptionStack.isEmpty()) {
+            Throwable currentException = exceptionStack.pop();
+            if (!previousExceptions.add(currentException)) {

Review Comment:
   The variable name `previousExceptions` in the context of a graph traversal 
is misleading. This set actually contains all visited exceptions during the 
traversal, not just "previous" exceptions. Consider renaming to 
`visitedExceptions` or `seenExceptions` to better convey its purpose as a 
cycle-prevention mechanism in the graph traversal.
   ```suggestion
           Set<Throwable> visitedExceptions = new HashSet<>();
           Deque<Throwable> exceptionStack = new ArrayDeque<>();
           exceptionStack.push(previous);
   
           while (!exceptionStack.isEmpty()) {
               Throwable currentException = exceptionStack.pop();
               if (!visitedExceptions.add(currentException)) {
   ```



##########
fluss-common/src/main/java/org/apache/fluss/utils/ExceptionUtils.java:
##########
@@ -365,10 +369,65 @@ public static <T extends Throwable> T firstOrSuppressed(T 
newException, @Nullabl
 
         if (previous == null || previous == newException) {
             return newException;
-        } else {
-            previous.addSuppressed(newException);
+        }
+
+        // If the exceptions already reference each other through the 
suppression or cause chains,
+        // return the previous exception to avoid introducing cycles.
+        if (existsInExceptionChain(newException, previous)
+                || existsInExceptionChain(previous, newException)) {
             return previous;
         }
+
+        previous.addSuppressed(newException);
+        return previous;
+    }
+
+    /**
+     * Checks whether the given {@code exception} throwable exception exists 
anywhere within the
+     * exception chain of {@code previous}. This includes both the cause chain 
and all suppressed
+     * exceptions. A visited set is used to avoid cycles and redundant 
traversal.
+     *
+     * @param exception The throwable exception to search for.
+     * @param previous The previous throwable exception chain to search in.

Review Comment:
   The parameter names in the javadoc don't accurately describe their 
semantics. The parameter named `exception` is described as "The throwable 
exception to search for" and `previous` is described as "The previous throwable 
exception chain to search in". However, these names suggest temporal ordering 
rather than the search relationship. Consider renaming them to something like 
`searchTarget` and `chainToSearch` or `needle` and `haystack` to better convey 
that this is a search operation, or update the description to clarify the 
purpose more explicitly.



##########
fluss-common/src/test/java/org/apache/fluss/utils/ExceptionUtilsTest.java:
##########
@@ -121,6 +122,35 @@ void testFirstOrSuppressed() {
         assertThat(suppressedException.getSuppressed()).isEqualTo(new 
Throwable[] {newException});
     }
 
+    @Test
+    void testFirstOrSuppressedCyclePrevention() {
+        // create two test exceptions (assuming thrown during shutdown, etc.)
+        Exception exceptionA = new Exception("Exception A");
+        Exception exceptionB = new Exception("Exception B");
+
+        // associate the suppressions (creating a suppression chain)
+        ExceptionUtils.firstOrSuppressed(exceptionB, exceptionA);
+        assertThat(exceptionA.getSuppressed()).contains(exceptionB);
+
+        // attempt to create a suppression cycle (A -> B; B -> A)
+        Exception result = ExceptionUtils.firstOrSuppressed(exceptionA, 
exceptionB);
+        assertThat(result).isEqualTo(exceptionB);
+
+        // verify the exception cycle was prevented (no bidirectional 
reference)
+        assertThat(exceptionA.getSuppressed()).contains(exceptionB);
+        assertThat(exceptionB.getSuppressed()).doesNotContain(exceptionA);
+        assertThat(exceptionB.getSuppressed()).isEmpty();
+
+        // verify that processing suppressed exceptions no longer causes 
StackOverflowError
+        Assertions.assertDoesNotThrow(() -> 
recursivelyProcessSuppressedExceptions(exceptionA));

Review Comment:
   The test uses `Assertions.assertDoesNotThrow` from JUnit Jupiter while the 
rest of the test class consistently uses AssertJ's assertion methods (like 
`assertThat`). For consistency, consider using AssertJ's equivalent approach or 
stick with the existing assertion pattern used throughout the file.



##########
fluss-common/src/main/java/org/apache/fluss/utils/ExceptionUtils.java:
##########
@@ -365,10 +369,65 @@ public static <T extends Throwable> T firstOrSuppressed(T 
newException, @Nullabl
 
         if (previous == null || previous == newException) {
             return newException;
-        } else {
-            previous.addSuppressed(newException);
+        }
+
+        // If the exceptions already reference each other through the 
suppression or cause chains,
+        // return the previous exception to avoid introducing cycles.
+        if (existsInExceptionChain(newException, previous)
+                || existsInExceptionChain(previous, newException)) {
             return previous;
         }
+
+        previous.addSuppressed(newException);
+        return previous;
+    }
+
+    /**
+     * Checks whether the given {@code exception} throwable exception exists 
anywhere within the
+     * exception chain of {@code previous}. This includes both the cause chain 
and all suppressed
+     * exceptions. A visited set is used to avoid cycles and redundant 
traversal.
+     *
+     * @param exception The throwable exception to search for.
+     * @param previous The previous throwable exception chain to search in.
+     * @return True, if the exception is found within the suppressed chain, 
false otherwise.

Review Comment:
   The documentation of the return value for `existsInExceptionChain` is 
inaccurate. The javadoc says "if the exception is found within the suppressed 
chain" but the method actually searches through both the suppressed chain AND 
the cause chain, which should be reflected in the documentation.
   ```suggestion
        * @return {@code true} if the exception is found within the exception 
chain (suppressed or
        *     cause), {@code false} otherwise.
   ```



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