This is an automated email from the ASF dual-hosted git repository.

jerryshao pushed a commit to branch branch-1.3
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/branch-1.3 by this push:
     new e7cd3f60ff [branch-1.3] [#13075] fix(catalog): inspect thread targets 
without resolving subclass fields (#13077)
e7cd3f60ff is described below

commit e7cd3f60ffce4bbf33fabd352c01876c348edbaf
Author: Qi Yu <[email protected]>
AuthorDate: Thu Sep 10 18:33:33 2026 +0800

    [branch-1.3] [#13075] fix(catalog): inspect thread targets without 
resolving subclass fields (#13077)
    
    ### What changes were proposed in this pull request?
    
    Cherry-pick #13076 (0e7e46360e) to branch-1.3 so its CI can validate the
    fix independently of main.
    
    Read the runnable from `Thread`'s declared `target` field, avoiding
    subclass field resolution and shadowed fields. Catch `LinkageError`
    during individual thread ownership checks so remaining cleanup can
    continue, while allowing fatal VM errors to propagate.
    
    Add regressions for missing subclass dependencies, shadowed targets,
    linkage errors, and fatal-error propagation.
    
    ### Why are the changes needed?
    
    Reflecting on an unrelated Hadoop thread can throw
    `NoClassDefFoundError` during catalog cleanup and fail a DISABLE
    request. A shadowed field can also cause the wrong thread ownership
    decision.
    
    Related issue: #13075
    
    ### Does this PR introduce _any_ user-facing change?
    
    Catalog cleanup no longer fails for these thread-inspection errors. No
    public API or configuration changes.
    
    ### How was this patch tested?
    
    - Regression tests cover missing subclass dependencies, shadowed
    targets, linkage errors, and fatal-error propagation. Three regressions
    fail before the fix.
    - `./gradlew spotlessApply :catalogs:catalog-common:check -PskipITs
    -PskipDockerTests=true` — 21 tests passed on branch-1.3.
    - `git diff --check` passed. Full Paimon integration validation is
    pending this PR's CI.
    
    Signed-off-by: yuqi <[email protected]>
---
 .../utils/ClassLoaderResourceCleanerUtils.java     |  19 ++--
 .../utils/TestClassLoaderResourceCleanerUtils.java | 103 +++++++++++++++++++++
 2 files changed, 115 insertions(+), 7 deletions(-)

diff --git 
a/catalogs/catalog-common/src/main/java/org/apache/gravitino/utils/ClassLoaderResourceCleanerUtils.java
 
b/catalogs/catalog-common/src/main/java/org/apache/gravitino/utils/ClassLoaderResourceCleanerUtils.java
index b4c33ffa8a..cff678a05c 100644
--- 
a/catalogs/catalog-common/src/main/java/org/apache/gravitino/utils/ClassLoaderResourceCleanerUtils.java
+++ 
b/catalogs/catalog-common/src/main/java/org/apache/gravitino/utils/ClassLoaderResourceCleanerUtils.java
@@ -197,17 +197,22 @@ public class ClassLoaderResourceCleanerUtils {
     if (thread == null) {
       return false;
     }
-    if (thread.getContextClassLoader() == targetClassLoader
-        || thread.getClass().getClassLoader() == targetClassLoader) {
-      return true;
-    }
     try {
-      Object runnable = FieldUtils.readField(thread, "target", true);
+      if (thread.getContextClassLoader() == targetClassLoader
+          || thread.getClass().getClassLoader() == targetClassLoader) {
+        return true;
+      }
+      // Inspect Thread's own target, not a subclass's fields. Reflecting on a 
driver thread
+      // subclass can resolve unavailable field types, or find a shadowed 
target field.
+      Field targetField = Thread.class.getDeclaredField("target");
+      targetField.setAccessible(true);
+      Object runnable = targetField.get(thread);
       if (runnable != null && runnable.getClass().getClassLoader() == 
targetClassLoader) {
         return true;
       }
-    } catch (Exception e) {
-      LOG.debug("Cannot read the runnable of thread {}", thread.getName(), e);
+    } catch (Exception | LinkageError e) {
+      // A stale thread's dependencies must not abort cleanup of the remaining 
threads.
+      LOG.debug("Cannot inspect the classloader ownership of thread {}", 
thread.getName(), e);
     }
 
     return false;
diff --git 
a/catalogs/catalog-common/src/test/java/org/apache/gravitino/utils/TestClassLoaderResourceCleanerUtils.java
 
b/catalogs/catalog-common/src/test/java/org/apache/gravitino/utils/TestClassLoaderResourceCleanerUtils.java
index 52e4d10b6a..953a75fdcf 100644
--- 
a/catalogs/catalog-common/src/test/java/org/apache/gravitino/utils/TestClassLoaderResourceCleanerUtils.java
+++ 
b/catalogs/catalog-common/src/test/java/org/apache/gravitino/utils/TestClassLoaderResourceCleanerUtils.java
@@ -23,6 +23,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 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;
 
 import java.lang.ref.SoftReference;
@@ -53,6 +54,108 @@ class TestClassLoaderResourceCleanerUtils {
     }
   }
 
+  /** A thread whose unrelated field type can become unavailable during 
classloader cleanup. */
+  public static class ThreadWithMissingDependency extends Thread {
+    /** A field whose type the isolated test loader deliberately cannot 
resolve. */
+    public Leaky dependency;
+
+    /**
+     * Creates a thread with the supplied runnable.
+     *
+     * @param runnable the actual thread target
+     */
+    public ThreadWithMissingDependency(Runnable runnable) {
+      super(runnable);
+    }
+  }
+
+  private static class ThreadWithShadowedTarget extends Thread {
+    @SuppressWarnings("unused")
+    private final Runnable target;
+
+    private ThreadWithShadowedTarget(Runnable actualTarget, Runnable 
shadowedTarget) {
+      super(actualTarget);
+      target = shadowedTarget;
+      setContextClassLoader(null);
+    }
+  }
+
+  @Test
+  void testThreadInspectionDoesNotResolveSubclassFieldTypes() throws Exception 
{
+    URL location = 
getClass().getProtectionDomain().getCodeSource().getLocation();
+    try (URLClassLoader child =
+        new URLClassLoader(new URL[] {location}, null) {
+          @Override
+          protected Class<?> loadClass(String name, boolean resolve) throws 
ClassNotFoundException {
+            if (name.equals(Leaky.class.getName())) {
+              throw new ClassNotFoundException(name);
+            }
+            return super.loadClass(name, resolve);
+          }
+        }) {
+      Thread thread =
+          (Thread)
+              child
+                  .loadClass(ThreadWithMissingDependency.class.getName())
+                  .getConstructor(Runnable.class)
+                  .newInstance((Runnable) () -> {});
+      thread.setContextClassLoader(null);
+      assertThrows(NoClassDefFoundError.class, () -> 
thread.getClass().getDeclaredFields());
+      assertTrue(
+          ClassLoaderResourceCleanerUtils.runningWithClassLoader(
+              thread, getClass().getClassLoader()));
+    }
+  }
+
+  @Test
+  void testThreadInspectionIgnoresShadowedTarget() throws Exception {
+    try (URLClassLoader child = childLoaderOwning(LeakyTask.class)) {
+      Runnable owned =
+          (Runnable)
+              
child.loadClass(LeakyTask.class.getName()).getDeclaredConstructor().newInstance();
+      Runnable unrelated = () -> {};
+      assertTrue(
+          ClassLoaderResourceCleanerUtils.runningWithClassLoader(
+              new ThreadWithShadowedTarget(owned, unrelated), child));
+      assertFalse(
+          ClassLoaderResourceCleanerUtils.runningWithClassLoader(
+              new ThreadWithShadowedTarget(unrelated, owned), child));
+    }
+  }
+
+  @Test
+  void testThreadInspectionToleratesLinkageErrors() {
+    Thread thread =
+        new Thread() {
+          @Override
+          public ClassLoader getContextClassLoader() {
+            throw new NoClassDefFoundError("unavailable driver dependency");
+          }
+        };
+    assertFalse(
+        ClassLoaderResourceCleanerUtils.runningWithClassLoader(
+            thread, getClass().getClassLoader()));
+  }
+
+  @Test
+  void testThreadInspectionDoesNotSwallowFatalErrors() {
+    OutOfMemoryError failure = new OutOfMemoryError("test error");
+    Thread thread =
+        new Thread() {
+          @Override
+          public ClassLoader getContextClassLoader() {
+            throw failure;
+          }
+        };
+    assertSame(
+        failure,
+        assertThrows(
+            OutOfMemoryError.class,
+            () ->
+                ClassLoaderResourceCleanerUtils.runningWithClassLoader(
+                    thread, getClass().getClassLoader())));
+  }
+
   private static URLClassLoader childLoaderOwning(Class<?> clazz) throws 
Exception {
     URL location = clazz.getProtectionDomain().getCodeSource().getLocation();
     // A null parent keeps delegation off the app loader, so the child defines 
the class itself.

Reply via email to