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

deniskuzZ pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hive.git


The following commit(s) were added to refs/heads/master by this push:
     new 871722957ac HIVE-29708: HMS Memory leak: Ensure 
FileSystem.closeAllForUGI is invoked on proxy user failure (#6590)
871722957ac is described below

commit 871722957aca1d21402bbde4fc91a1f9bae51db1
Author: Indhumathi <[email protected]>
AuthorDate: Sun Jul 12 12:32:51 2026 +0530

    HIVE-29708: HMS Memory leak: Ensure FileSystem.closeAllForUGI is invoked on 
proxy user failure (#6590)
---
 .../hadoop/hive/ql/txn/compactor/MRCompactor.java  |  21 +-
 .../hadoop/hive/ql/txn/compactor/Worker.java       |  17 +-
 .../txn/compactor/TestFileSystemCacheLeakFix.java  | 276 +++++++++++++++++++++
 .../apache/hadoop/hive/metastore/txn/TxnUtils.java |  24 +-
 4 files changed, 311 insertions(+), 27 deletions(-)

diff --git 
a/ql/src/java/org/apache/hadoop/hive/ql/txn/compactor/MRCompactor.java 
b/ql/src/java/org/apache/hadoop/hive/ql/txn/compactor/MRCompactor.java
index d3bcbf42c01..274d728894f 100644
--- a/ql/src/java/org/apache/hadoop/hive/ql/txn/compactor/MRCompactor.java
+++ b/ql/src/java/org/apache/hadoop/hive/ql/txn/compactor/MRCompactor.java
@@ -126,16 +126,19 @@ public boolean run(CompactorContext context)
     } else {
       UserGroupInformation ugi = 
UserGroupInformation.createProxyUser(context.getCompactionInfo().runAs,
               UserGroupInformation.getLoginUser());
-      ugi.doAs((PrivilegedExceptionAction<Object>) () -> {
-        run(context.getConf(), context.getTable(), context.getSd(),
-                context.getValidWriteIdList(), context.getCompactionInfo(), 
context.getAcidDirectory());
-        return null;
-      });
       try {
-        FileSystem.closeAllForUGI(ugi);
-      } catch (IOException exception) {
-        LOG.error("Could not clean up file-system handles for UGI: " + ugi + " 
for " + context.getCompactionInfo().getFullPartitionName(),
-            exception);
+        ugi.doAs((PrivilegedExceptionAction<Void>) () -> {
+          run(context.getConf(), context.getTable(), context.getSd(),
+                  context.getValidWriteIdList(), context.getCompactionInfo(), 
context.getAcidDirectory());
+          return null;
+        });
+      } finally {
+        try {
+          FileSystem.closeAllForUGI(ugi);
+        } catch (IOException exception) {
+          LOG.error("Could not clean up file-system handles for UGI: " + ugi + 
" for " + context.getCompactionInfo().getFullPartitionName(),
+                  exception);
+        }
       }
     }
     return true;
diff --git a/ql/src/java/org/apache/hadoop/hive/ql/txn/compactor/Worker.java 
b/ql/src/java/org/apache/hadoop/hive/ql/txn/compactor/Worker.java
index 80c978fc569..4bf8704d703 100644
--- a/ql/src/java/org/apache/hadoop/hive/ql/txn/compactor/Worker.java
+++ b/ql/src/java/org/apache/hadoop/hive/ql/txn/compactor/Worker.java
@@ -257,14 +257,17 @@ protected Boolean findNextCompactionAndExecute(boolean 
collectGenericStats, bool
 
           CompactionService finalCompactionService = compactionService;
           CompactionInfo finalCi = ci;
-          ugi.doAs((PrivilegedExceptionAction<Void>) () -> {
-            finalCompactionService.cleanupResultDirs(finalCi);
-            return null;
-          });
           try {
-            FileSystem.closeAllForUGI(ugi);
-          } catch (IOException ex) {
-            LOG.error("Could not clean up file-system handles for UGI: " + 
ugi, e);
+            ugi.doAs((PrivilegedExceptionAction<Void>) () -> {
+              finalCompactionService.cleanupResultDirs(finalCi);
+              return null;
+            });
+          } finally {
+            try {
+              FileSystem.closeAllForUGI(ugi);
+            } catch (IOException ex) {
+              LOG.error("Could not clean up file-system handles for UGI: " + 
ugi, e);
+            }
           }
         }
       }
diff --git 
a/ql/src/test/org/apache/hadoop/hive/ql/txn/compactor/TestFileSystemCacheLeakFix.java
 
b/ql/src/test/org/apache/hadoop/hive/ql/txn/compactor/TestFileSystemCacheLeakFix.java
new file mode 100644
index 00000000000..b6e1ca6bccf
--- /dev/null
+++ 
b/ql/src/test/org/apache/hadoop/hive/ql/txn/compactor/TestFileSystemCacheLeakFix.java
@@ -0,0 +1,276 @@
+/*
+ * 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.hadoop.hive.ql.txn.compactor;
+
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.FSDataInputStream;
+import org.apache.hadoop.fs.FSDataOutputStream;
+import org.apache.hadoop.fs.FileStatus;
+import org.apache.hadoop.fs.FileSystem;
+import org.apache.hadoop.fs.Path;
+import org.apache.hadoop.fs.permission.FsPermission;
+import org.apache.hadoop.hive.common.ValidReaderWriteIdList;
+import org.apache.hadoop.hive.common.ValidWriteIdList;
+import org.apache.hadoop.hive.conf.HiveConf;
+import org.apache.hadoop.hive.metastore.IMetaStoreClient;
+import org.apache.hadoop.hive.metastore.api.CompactionType;
+import org.apache.hadoop.hive.metastore.api.StorageDescriptor;
+import org.apache.hadoop.hive.metastore.api.Table;
+import org.apache.hadoop.hive.metastore.txn.TxnUtils;
+import org.apache.hadoop.hive.metastore.txn.entities.CompactionInfo;
+import org.apache.hadoop.hive.ql.io.AcidDirectory;
+import org.apache.hadoop.security.AccessControlException;
+import org.apache.hadoop.util.Progressable;
+import org.junit.Assert;
+import org.junit.Test;
+import org.mockito.invocation.InvocationOnMock;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.net.URI;
+import java.util.Map;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+
+/**
+ * Regression tests for HIVE-29708: FileSystem.CACHE leak due to missing 
finally block
+ * in three compactor locations - TxnUtils.findUserToRunAs(), 
MRCompactor.run().
+ */
+public class TestFileSystemCacheLeakFix {
+  private static final String TEST_USER = "testowner";
+  private static final int CYCLES = 5;
+
+  @Test
+  public void testTxnUtilsNoFileSystemCacheLeakWhenProxyDoAsFails() throws 
Exception {
+    // Verify FileSystem.CACHE does not grow when proxy user also lacks HDFS 
permission,
+    // causing doAs() to throw and closeAllForUGI to be skipped in 
findUserToRunAs().
+    HiveConf conf = buildConf(AccessDeniedFileSystem.class, 
AccessDeniedFileSystem.SCHEME);
+    Table table = buildTable(AccessDeniedFileSystem.SCHEME);
+    assertNoCacheGrowth("TxnUtils.findUserToRunAs",
+         () -> TxnUtils.findUserToRunAs(table.getSd().getLocation(), table, 
conf));
+  }
+
+  @Test
+  public void testTxnUtilsNoFileSystemCacheLeakWhenServerUserSucceeds() throws 
Exception {
+    // Verify FileSystem.CACHE does not grow when server user successfully 
stats the
+    // table location - no proxy UGI is created, so no CACHE entry is added.
+    HiveConf conf = buildConf(AccessGrantedFileSystem.class, 
AccessGrantedFileSystem.SCHEME);
+    Table table = buildTable(AccessGrantedFileSystem.SCHEME);
+    assertNoCacheGrowth("TxnUtils.findUserToRunAs (success path)", () -> {
+      String owner = TxnUtils.findUserToRunAs(table.getSd().getLocation(), 
table, conf);
+      Assert.assertEquals(AccessGrantedFileSystem.FAKE_OWNER, owner);
+    });
+  }
+
+  @Test
+  public void testMRCompactorNoFileSystemCacheLeakWhenRunDoAsFails() throws 
Exception {
+    // Verify FileSystem.CACHE does not grow when the actual 
MRCompactor.run(CompactorContext)
+    // proxy path accesses FileSystem inside doAs() and then throws.
+    // The inner run() is spied to force FileSystem.get() before throwing, 
ensuring the
+    // proxy UGI CACHE entry is created and must be cleaned up via 
closeAllForUGI.
+    HiveConf conf = buildConf(AccessDeniedFileSystem.class, 
AccessDeniedFileSystem.SCHEME);
+    MRCompactor compactor = spy(new MRCompactor(mock(IMetaStoreClient.class)));
+    doAnswer((InvocationOnMock inv) -> {
+      new Path(AccessDeniedFileSystem.SCHEME + 
"://host/table").getFileSystem(conf);
+      throw new IOException("simulated MR failure");
+    }).when(compactor).run(
+                any(HiveConf.class),
+                any(Table.class),
+                any(StorageDescriptor.class),
+                any(ValidWriteIdList.class),
+                any(CompactionInfo.class),
+                any(AcidDirectory.class));
+
+    assertNoCacheGrowth("MRCompactor.run", () -> {
+      try {
+        compactor.run(buildCompactorContext());
+      } catch (Exception ignored) {
+         // ignore
+      }
+    });
+  }
+
+  private void assertNoCacheGrowth(String ctx, RunnableWithException op) 
throws Exception {
+    try {
+      op.run();
+    } catch (Exception ignored) {
+        // ignore
+    }
+    int before = getFileSystemCacheSize();
+    for (int i = 0; i < TestFileSystemCacheLeakFix.CYCLES; i++) {
+      try {
+        op.run();
+      } catch (Exception ignored) {
+          // ignore
+      }
+    }
+    int after = getFileSystemCacheSize();
+    Assert.assertEquals(ctx + ": CACHE grew by " + (after - before) + " 
(expected 0)", before, after);
+  }
+
+  private static HiveConf buildConf(Class<? extends FileSystem> fsClass, 
String scheme) {
+    HiveConf conf = new HiveConf();
+    conf.set("fs." + scheme + ".impl", fsClass.getName());
+    conf.set("fs." + scheme + ".impl.disable.cache", "false");
+    return conf;
+  }
+
+  private static Table buildTable(String scheme) {
+    StorageDescriptor sd = new StorageDescriptor();
+    sd.setLocation(scheme + ":///warehouse/testdb.db/testtable");
+    Table table = new Table();
+    table.setDbName("testdb");
+    table.setTableName("testtable");
+    table.setOwner(TestFileSystemCacheLeakFix.TEST_USER);
+    table.setSd(sd);
+    return table;
+  }
+
+  private static CompactorContext buildCompactorContext() {
+    HiveConf conf = buildConf(AccessDeniedFileSystem.class, 
AccessDeniedFileSystem.SCHEME);
+    StorageDescriptor sd = new StorageDescriptor();
+    sd.setLocation(AccessDeniedFileSystem.SCHEME + 
":///warehouse/testdb.db/testtable");
+    Table table = new Table();
+    table.setDbName("testdb");
+    table.setTableName("testtable");
+    table.setOwner(TestFileSystemCacheLeakFix.TEST_USER);
+    table.setSd(sd);
+    CompactionInfo ci = new CompactionInfo("testdb", "testtable", null, 
CompactionType.MAJOR);
+    ci.runAs = TestFileSystemCacheLeakFix.TEST_USER;
+    return new CompactorContext(conf, table, null, sd, new 
ValidReaderWriteIdList(), ci, mock(AcidDirectory.class));
+  }
+
+  static int getFileSystemCacheSize() throws Exception {
+    Field cacheField = FileSystem.class.getDeclaredField("CACHE");
+    cacheField.setAccessible(true);
+    Object cache = cacheField.get(null);
+    Field mapField = cache.getClass().getDeclaredField("map");
+    mapField.setAccessible(true);
+    return ((Map<?, ?>) mapField.get(cache)).size();
+  }
+
+  @FunctionalInterface
+  interface RunnableWithException {
+    void run() throws Exception;
+  }
+
+  /* Mock FileSystem that always throws AccessControlException on 
getFileStatus,
+  * simulating an HDFS directory with drwx------ permissions.
+  */
+  static class AccessDeniedFileSystem extends FileSystem {
+    public static final String SCHEME = "testcachefix";
+
+    @Override
+     public URI getUri() {
+      return URI.create(SCHEME + ":///");
+    }
+
+    @Override
+    public FileStatus getFileStatus(Path f) throws IOException {
+      throw new AccessControlException("Permission denied: access=EXECUTE, 
inode=\"" + f + "\":hdfs:hdfs:drwx------");
+    }
+
+    @Override public void initialize(URI uri, Configuration conf) throws 
IOException {
+      // No-op: this mock does not require real FS initialization
+    }
+    @Override public FSDataInputStream open(Path f, int bufferSize) throws 
IOException {
+      return null;
+    }
+    @Override public FSDataOutputStream create(Path f, FsPermission 
permission, boolean overwrite, int bufferSize,
+                                               short replication, long 
blockSize,
+                                               Progressable progress) throws 
IOException {
+      return null;
+    }
+    @Override public FSDataOutputStream append(Path f, int bufferSize, 
Progressable progress) throws IOException {
+      return null;
+    }
+    @Override public boolean rename(Path src, Path dst) throws IOException {
+      return false;
+    }
+    @Override public boolean delete(Path f, boolean recursive) throws 
IOException {
+      return false;
+    }
+    @Override public FileStatus[] listStatus(Path f) throws IOException {
+      return new FileStatus[0];
+    }
+    @Override public void setWorkingDirectory(Path newDir) {
+      // No-op: working directory is not used by this mock FileSystem
+    }
+    @Override public Path getWorkingDirectory() {
+      return new Path(SCHEME + ":///");
+    }
+    @Override public boolean mkdirs(Path f, FsPermission permission) throws 
IOException {
+      return false;
+    }
+  }
+
+  /**
+   * Mock FileSystem that always succeeds on getFileStatus, returning a fixed 
owner.
+  */
+  static class AccessGrantedFileSystem extends FileSystem {
+    public static final String SCHEME = "testsuccess";
+    public static final String FAKE_OWNER = "tableowner";
+
+    @Override
+    public URI getUri() {
+      return URI.create(SCHEME + ":///");
+    }
+
+    @Override
+    public FileStatus getFileStatus(Path f) throws IOException {
+      return new FileStatus(0, true, 0, 0, 0, 0, FsPermission.getDirDefault(), 
FAKE_OWNER, FAKE_OWNER, f);
+    }
+
+    @Override public void initialize(URI uri, Configuration conf) throws 
IOException {
+      // No-op: this mock does not require real FS initialization
+    }
+    @Override public FSDataInputStream open(Path f, int bufferSize) throws 
IOException {
+      return null;
+    }
+    @Override public FSDataOutputStream create(Path f, FsPermission 
permission, boolean overwrite, int bufferSize,
+                                               short replication, long 
blockSize,
+                                               Progressable progress) throws 
IOException {
+      return null;
+    }
+    @Override public FSDataOutputStream append(Path f, int bufferSize, 
Progressable progress) throws IOException {
+      return null;
+    }
+    @Override public boolean rename(Path src, Path dst) throws IOException {
+      return false;
+    }
+    @Override public boolean delete(Path f, boolean recursive) throws 
IOException {
+      return false;
+    }
+    @Override public FileStatus[] listStatus(Path f) throws IOException {
+      return new FileStatus[0];
+    }
+    @Override public void setWorkingDirectory(Path newDir) {
+      // No-op: working directory is not used by this mock FileSystem
+    }
+    @Override public Path getWorkingDirectory() {
+      return new Path(SCHEME + ":///");
+    }
+    @Override public boolean mkdirs(Path f, FsPermission permission) throws 
IOException {
+      return false;
+    }
+  }
+}
diff --git 
a/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/txn/TxnUtils.java
 
b/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/txn/TxnUtils.java
index 5c5ca7c7bfa..79d6900222b 100644
--- 
a/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/txn/TxnUtils.java
+++ 
b/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/txn/TxnUtils.java
@@ -572,18 +572,20 @@ public static String findUserToRunAs(String location, 
Table t, Configuration con
       UserGroupInformation ugi = 
UserGroupInformation.createProxyUser(t.getOwner(),
         UserGroupInformation.getLoginUser());
 
-      ugi.doAs((PrivilegedExceptionAction<Void>) () -> {
-        // need to use a new filesystem object here to have the correct ugi
-        FileSystem proxyFs = p.getFileSystem(conf);
-        FileStatus stat = proxyFs.getFileStatus(p);
-        wrapper.add(stat.getOwner());
-        return null;
-      });
-
       try {
-        FileSystem.closeAllForUGI(ugi);
-      } catch (IOException exception) {
-        LOG.error("Could not clean up file-system handles for UGI: " + ugi, 
exception);
+        ugi.doAs((PrivilegedExceptionAction<Void>) () -> {
+          // need to use a new filesystem object here to have the correct ugi
+          FileSystem proxyFs = p.getFileSystem(conf);
+          FileStatus stat = proxyFs.getFileStatus(p);
+          wrapper.add(stat.getOwner());
+          return null;
+        });
+      } finally {
+        try {
+          FileSystem.closeAllForUGI(ugi);
+        } catch (IOException exception) {
+          LOG.error("Could not clean up file-system handles for UGI: " + ugi, 
exception);
+        }
       }
       if (wrapper.size() == 1) {
         LOG.debug("Running job as {}", wrapper.get(0));

Reply via email to