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 c82d027271 [Cherry-pick to branch-1.3] [#12246] fix(catalog-hive): 
Close Log4j LoggerContext in HiveClientClassLoader#close() to prevent Metaspace 
leak (#12925) (#12951)
c82d027271 is described below

commit c82d0272715510c31e5937531f809e6de34e8661
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Mon Sep 7 20:25:23 2026 +0800

    [Cherry-pick to branch-1.3] [#12246] fix(catalog-hive): Close Log4j 
LoggerContext in HiveClientClassLoader#close() to prevent Metaspace leak 
(#12925) (#12951)
    
    **Cherry-pick Information:**
    - Original commit: 096196e09d2114786511fe5065de61982105a0f4
    - Target branch: `branch-1.3`
    - Status: ✅ Clean cherry-pick (no conflicts)
    
    Co-authored-by: Yuhui <[email protected]>
    Co-authored-by: Claude Sonnet 5 <[email protected]>
---
 catalogs/hive-metastore-common/build.gradle.kts    |  1 +
 .../hive/client/HiveClientClassLoader.java         | 28 ++++++++
 .../hive/client/TestHiveClientClassLoader.java     | 83 ++++++++++++++++++++++
 3 files changed, 112 insertions(+)

diff --git a/catalogs/hive-metastore-common/build.gradle.kts 
b/catalogs/hive-metastore-common/build.gradle.kts
index 1479f18e96..467396facc 100644
--- a/catalogs/hive-metastore-common/build.gradle.kts
+++ b/catalogs/hive-metastore-common/build.gradle.kts
@@ -43,6 +43,7 @@ dependencies {
   compileOnly(libs.caffeine)
   compileOnly(libs.guava)
   compileOnly(libs.slf4j.api)
+  compileOnly(libs.log4j.api)
 
   implementation(project(":catalogs:catalog-common")) {
     exclude("*")
diff --git 
a/catalogs/hive-metastore-common/src/main/java/org/apache/gravitino/hive/client/HiveClientClassLoader.java
 
b/catalogs/hive-metastore-common/src/main/java/org/apache/gravitino/hive/client/HiveClientClassLoader.java
index 0e965f1e52..aa18c95340 100644
--- 
a/catalogs/hive-metastore-common/src/main/java/org/apache/gravitino/hive/client/HiveClientClassLoader.java
+++ 
b/catalogs/hive-metastore-common/src/main/java/org/apache/gravitino/hive/client/HiveClientClassLoader.java
@@ -28,6 +28,7 @@ import java.util.List;
 import java.util.stream.Collectors;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.gravitino.exceptions.GravitinoRuntimeException;
+import org.apache.logging.log4j.LogManager;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -150,6 +151,33 @@ public final class HiveClientClassLoader extends 
URLClassLoader {
     }
   }
 
+  @Override
+  public void close() throws IOException {
+    try {
+      shutdownLog4jContext();
+    } finally {
+      super.close();
+    }
+  }
+
+  /**
+   * When a barrier class defined by this loader (e.g. {@link Util}, {@link 
HiveClientImpl}) first
+   * logs, log4j-slf4j2-impl's {@code Log4jLoggerFactory} resolves the caller 
class and creates a
+   * {@link org.apache.logging.log4j.spi.LoggerContext} keyed by that class's 
defining classloader,
+   * i.e. this loader. That factory keeps a strong reference from the {@code 
LoggerContext} to its
+   * internal per-logger registry, so the context - and transitively this 
classloader - stays
+   * reachable even after {@link #close()} closes the jar handles, preventing 
the classloader and
+   * its loaded classes from ever being collected. Explicitly shutting the 
context down here removes
+   * that registration.
+   */
+  private void shutdownLog4jContext() {
+    try {
+      LogManager.shutdown(LogManager.getContext(this, false));
+    } catch (Throwable t) {
+      LOG.warn("Failed to shut down Log4j context for classloader {}", 
getName(), t);
+    }
+  }
+
   @Override
   protected Class<?> loadClass(String name, boolean resolve) throws 
ClassNotFoundException {
     Class<?> loaded = findLoadedClass(name);
diff --git 
a/catalogs/hive-metastore-common/src/test/java/org/apache/gravitino/hive/client/TestHiveClientClassLoader.java
 
b/catalogs/hive-metastore-common/src/test/java/org/apache/gravitino/hive/client/TestHiveClientClassLoader.java
new file mode 100644
index 0000000000..2cb98de374
--- /dev/null
+++ 
b/catalogs/hive-metastore-common/src/test/java/org/apache/gravitino/hive/client/TestHiveClientClassLoader.java
@@ -0,0 +1,83 @@
+/*
+ * 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.gravitino.hive.client;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.Map;
+import java.util.Properties;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.spi.LoggerContext;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.slf4j.ILoggerFactory;
+import org.slf4j.LoggerFactory;
+
+public class TestHiveClientClassLoader {
+
+  @Test
+  void testCloseRemovesLog4jLoggerContextRegistration() throws Exception {
+    HiveClientClassLoader classLoader =
+        HiveClientClassLoader.createLoader(
+            HiveClientClassLoader.HiveVersion.HIVE3, 
getClass().getClassLoader());
+
+    // Trigger initialization of Util, a barrier class that is defined 
directly inside this
+    // classloader (see HiveClientClassLoader#loadBarrierClass), just like 
HiveClientImpl and
+    // HiveShim are in production. Util's static logger initialization
+    // (LoggerFactory.getLogger(Util.class)) makes log4j-slf4j2-impl's 
Log4jLoggerFactory walk the
+    // call stack to Util as the anchor class and register a LoggerContext 
keyed by Util's
+    // defining classloader, i.e. this HiveClientClassLoader, in its registry 
map. That map is
+    // keyed by the LoggerContext object itself (a strong reference), so the 
LoggerContext - and
+    // through it this classloader - stays reachable until the context is 
explicitly shut down.
+    Class<?> utilClass = classLoader.loadClass(Util.class.getName());
+    Assertions.assertSame(
+        classLoader, utilClass.getClassLoader(), "Util should be defined by 
the isolated loader");
+    Method updateConfig =
+        utilClass.getMethod(
+            "updateConfigurationFromProperties", Properties.class, 
Configuration.class);
+    updateConfig.invoke(null, new Properties(), new Configuration());
+
+    LoggerContext context = LogManager.getContext(classLoader, false);
+    Map<LoggerContext, ?> registry = getSlf4jAdapterRegistry();
+    Assertions.assertTrue(
+        registry.containsKey(context),
+        "Precondition failed: Log4jLoggerFactory should have registered a 
LoggerContext for "
+            + "the isolated classloader");
+
+    classLoader.close();
+
+    Assertions.assertFalse(
+        registry.containsKey(context),
+        "HiveClientClassLoader#close() should shut down its Log4j 
LoggerContext so that "
+            + "Log4jLoggerFactory's registry releases its strong reference to 
it (and "
+            + "transitively to the classloader); otherwise the classloader can 
never be "
+            + "garbage collected");
+  }
+
+  /** Reflectively reads the {@code registry} field of {@code 
AbstractLoggerAdapter}. */
+  @SuppressWarnings("unchecked")
+  private static Map<LoggerContext, ?> getSlf4jAdapterRegistry() throws 
Exception {
+    ILoggerFactory adapter = LoggerFactory.getILoggerFactory();
+    Field registryField = 
adapter.getClass().getSuperclass().getDeclaredField("registry");
+    registryField.setAccessible(true);
+    return (Map<LoggerContext, ?>) registryField.get(adapter);
+  }
+}

Reply via email to