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);
+ }
+}