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

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


The following commit(s) were added to refs/heads/master by this push:
     new dd23feef9c4c fix: close the filesystem's stream on rewrap (#19935)
dd23feef9c4c is described below

commit dd23feef9c4c7a507da90110c53955fa34fe6869
Author: voonhous <[email protected]>
AuthorDate: Mon Sep 14 16:37:53 2026 +0800

    fix: close the filesystem's stream on rewrap (#19935)
    
    HadoopFSUtils.getFSDataInputStream unwraps the FSDataInputStream a
    filesystem returns, buffers its inner FSInputStream and hands out
    the buffered stream, so closing what Hudi returns never closes the
    object the filesystem created. Filesystems that track the streams
    they hand out, such as Spark's test DebugFilesystem, then report
    every Hudi log file read as leaked although the OS stream is
    closed. TestStreamingSource aborted on exactly that ("26 possibly
    leaked file streams") once it ran in a JVM where DebugFilesystem
    was the cached local filesystem; in the old serial job a plain
    LocalFileSystem got cached first and the check was vacuous.
    
    The returned stream now closes the original FSDataInputStream
    after the buffered wrapper; the inner stream's close is idempotent
    by the Closeable contract. A unit test checks the outer object is
    closed once.
---
 .../org/apache/hudi/hadoop/fs/HadoopFSUtils.java   | 15 +++++++-
 .../apache/hudi/hadoop/fs/TestHadoopFSUtils.java   | 45 ++++++++++++++++++++++
 2 files changed, 59 insertions(+), 1 deletion(-)

diff --git 
a/hudi-hadoop-common/src/main/java/org/apache/hudi/hadoop/fs/HadoopFSUtils.java 
b/hudi-hadoop-common/src/main/java/org/apache/hudi/hadoop/fs/HadoopFSUtils.java
index 8c0293dbe795..251a47db4055 100644
--- 
a/hudi-hadoop-common/src/main/java/org/apache/hudi/hadoop/fs/HadoopFSUtils.java
+++ 
b/hudi-hadoop-common/src/main/java/org/apache/hudi/hadoop/fs/HadoopFSUtils.java
@@ -231,8 +231,21 @@ public class HadoopFSUtils {
     }
 
     if (fsDataInputStream.getWrappedStream() instanceof FSInputStream) {
+      // The buffered stream wraps the inner FSInputStream, not the 
FSDataInputStream the filesystem
+      // returned. Filesystems that track the streams they hand out (Spark's 
DebugFilesystem in tests)
+      // see a leak unless that outer object is closed too; the inner stream's 
close is idempotent.
+      final FSDataInputStream original = fsDataInputStream;
       return new TimedFSDataInputStream(convertToHadoopPath(filePath), new 
FSDataInputStream(
-          new BufferedFSInputStream((FSInputStream) 
fsDataInputStream.getWrappedStream(), bufferSize)));
+          new BufferedFSInputStream((FSInputStream) 
original.getWrappedStream(), bufferSize)) {
+        @Override
+        public void close() throws IOException {
+          try {
+            super.close();
+          } finally {
+            original.close();
+          }
+        }
+      });
     }
 
     // fsDataInputStream.getWrappedStream() maybe a BufferedFSInputStream
diff --git 
a/hudi-hadoop-common/src/test/java/org/apache/hudi/hadoop/fs/TestHadoopFSUtils.java
 
b/hudi-hadoop-common/src/test/java/org/apache/hudi/hadoop/fs/TestHadoopFSUtils.java
index 92e7234eab7d..901d803d2375 100644
--- 
a/hudi-hadoop-common/src/test/java/org/apache/hudi/hadoop/fs/TestHadoopFSUtils.java
+++ 
b/hudi-hadoop-common/src/test/java/org/apache/hudi/hadoop/fs/TestHadoopFSUtils.java
@@ -31,6 +31,7 @@ import org.apache.hudi.storage.hadoop.HoodieHadoopStorage;
 
 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.FilterFileSystem;
@@ -181,6 +182,50 @@ public class TestHadoopFSUtils {
     }
   }
 
+  /**
+   * The buffered stream built for the {@code FSInputStream} branch wraps the 
inner stream, not the
+   * {@link FSDataInputStream} the filesystem returned, so closing it used to 
leave that outer object open.
+   * Filesystems that track the streams they hand out - Spark's {@code 
DebugFilesystem} in tests - reported
+   * every log file read as a leaked stream.
+   */
+  @Test
+  public void testGetFSDataInputStreamClosesTheFileSystemStream(@TempDir 
java.nio.file.Path tempDir) throws IOException {
+    byte[] contents = new byte[] {7, 8, 9};
+    try (TrackingLocalFileSystem fs = new TrackingLocalFileSystem()) {
+      fs.initialize(URI.create("file:///"), new Configuration());
+      Path file = new Path(tempDir.resolve("tracked.log").toUri());
+      try (FSDataOutputStream out = fs.create(file)) {
+        out.write(contents);
+      }
+
+      FSDataInputStream stream =
+          HadoopFSUtils.getFSDataInputStream(fs, new 
StoragePath(file.toUri()), 4096, true);
+      assertEquals(7, stream.read());
+      stream.close();
+      assertEquals(1, fs.closeCount.get(),
+          "closing the returned stream must also close the stream the 
filesystem handed out");
+    }
+  }
+
+  /** A {@link LocalFileSystem} that counts the closes of the streams it hands 
out. */
+  private static class TrackingLocalFileSystem extends LocalFileSystem {
+    private final AtomicInteger closeCount = new AtomicInteger();
+
+    @Override
+    public FSDataInputStream open(Path f, int bufferSize) throws IOException {
+      return new FSDataInputStream(super.open(f, 
bufferSize).getWrappedStream()) {
+        @Override
+        public void close() throws IOException {
+          try {
+            super.close();
+          } finally {
+            closeCount.incrementAndGet();
+          }
+        }
+      };
+    }
+  }
+
   /** A FileSystem with the reported shape: {@code getUri()} works, {@code 
getScheme()} throws. */
   private static FileSystem newFsWithoutGetScheme(FileSystem delegate) {
     FileSystem fs = new FilterFileSystem(delegate);

Reply via email to