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

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


The following commit(s) were added to refs/heads/master by this push:
     new 44bdd917cb8 [FLINK-40644][state/forst] Close the remote stream when a 
CachedDataInputStream is closed
44bdd917cb8 is described below

commit 44bdd917cb87202d54ed08487edf9b8a5ebf58a1
Author: Seungjoo Choi <[email protected]>
AuthorDate: Tue Oct 6 17:53:50 2026 +0900

    [FLINK-40644][state/forst] Close the remote stream when a 
CachedDataInputStream is closed
    
    Generated-by: Claude Code (Claude Fable 5.1)
---
 .../fs/ByteBufferReadableFSDataInputStream.java    |  18 ++-
 .../forst/fs/cache/CachedDataInputStream.java      |  24 +++-
 .../ByteBufferReadableFSDataInputStreamTest.java   |  99 +++++++++++++
 .../forst/fs/cache/CachedDataInputStreamTest.java  | 154 +++++++++++++++++++++
 4 files changed, 289 insertions(+), 6 deletions(-)

diff --git 
a/flink-state-backends/flink-statebackend-forst/src/main/java/org/apache/flink/state/forst/fs/ByteBufferReadableFSDataInputStream.java
 
b/flink-state-backends/flink-statebackend-forst/src/main/java/org/apache/flink/state/forst/fs/ByteBufferReadableFSDataInputStream.java
index 79d64bb78ba..70eb3f76014 100644
--- 
a/flink-state-backends/flink-statebackend-forst/src/main/java/org/apache/flink/state/forst/fs/ByteBufferReadableFSDataInputStream.java
+++ 
b/flink-state-backends/flink-statebackend-forst/src/main/java/org/apache/flink/state/forst/fs/ByteBufferReadableFSDataInputStream.java
@@ -20,6 +20,7 @@ package org.apache.flink.state.forst.fs;
 
 import org.apache.flink.core.fs.ByteBufferReadable;
 import org.apache.flink.core.fs.FSDataInputStream;
+import org.apache.flink.util.IOUtils;
 
 import java.io.IOException;
 import java.nio.ByteBuffer;
@@ -128,11 +129,18 @@ public class ByteBufferReadableFSDataInputStream extends 
FSDataInputStream {
         }
 
         int result;
-        if (fsDataInputStream instanceof ByteBufferReadable) {
-            result = ((ByteBufferReadable) fsDataInputStream).read(position, 
bb);
-        } else {
-            fsDataInputStream.seek(position);
-            result = readFullyFromFSDataInputStream(fsDataInputStream, bb);
+        try {
+            if (fsDataInputStream instanceof ByteBufferReadable) {
+                result = ((ByteBufferReadable) 
fsDataInputStream).read(position, bb);
+            } else {
+                fsDataInputStream.seek(position);
+                result = readFullyFromFSDataInputStream(fsDataInputStream, bb);
+            }
+        } catch (Exception ex) {
+            // The stream is neither returned to the pool nor closed 
otherwise: close it here so
+            // that a failed read does not leak the underlying (remote) stream.
+            IOUtils.closeQuietly(fsDataInputStream);
+            throw ex;
         }
 
         boolean offered;
diff --git 
a/flink-state-backends/flink-statebackend-forst/src/main/java/org/apache/flink/state/forst/fs/cache/CachedDataInputStream.java
 
b/flink-state-backends/flink-statebackend-forst/src/main/java/org/apache/flink/state/forst/fs/cache/CachedDataInputStream.java
index 625ca55fce9..da070128f0e 100644
--- 
a/flink-state-backends/flink-statebackend-forst/src/main/java/org/apache/flink/state/forst/fs/cache/CachedDataInputStream.java
+++ 
b/flink-state-backends/flink-statebackend-forst/src/main/java/org/apache/flink/state/forst/fs/cache/CachedDataInputStream.java
@@ -306,7 +306,29 @@ public class CachedDataInputStream extends 
FSDataInputStream implements ByteBuff
             return;
         }
         closed = true;
-        closeCachedStream();
+        // Both the cached stream and the original (remote) stream belong to 
this wrapper:
+        // FileCacheEntry#open hands the original stream over and keeps no 
reference to it, so
+        // nobody else can close it. Leaving it open leaks whatever the remote 
file system holds
+        // for it (for S3A: a leased connection of the HTTP connection pool). 
Close both, and if
+        // both fail keep the first exception and attach the second as 
suppressed.
+        IOException failure = null;
+        try {
+            closeCachedStream();
+        } catch (IOException e) {
+            failure = e;
+        }
+        try {
+            originalStream.close();
+        } catch (IOException e) {
+            if (failure == null) {
+                failure = e;
+            } else {
+                failure.addSuppressed(e);
+            }
+        }
+        if (failure != null) {
+            throw failure;
+        }
     }
 
     public boolean isClosed() {
diff --git 
a/flink-state-backends/flink-statebackend-forst/src/test/java/org/apache/flink/state/forst/fs/ByteBufferReadableFSDataInputStreamTest.java
 
b/flink-state-backends/flink-statebackend-forst/src/test/java/org/apache/flink/state/forst/fs/ByteBufferReadableFSDataInputStreamTest.java
new file mode 100644
index 00000000000..2f663bf312b
--- /dev/null
+++ 
b/flink-state-backends/flink-statebackend-forst/src/test/java/org/apache/flink/state/forst/fs/ByteBufferReadableFSDataInputStreamTest.java
@@ -0,0 +1,99 @@
+/*
+ * 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.flink.state.forst.fs;
+
+import org.apache.flink.core.fs.FSDataInputStream;
+
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/** Tests for the stream pool of {@link ByteBufferReadableFSDataInputStream}. 
*/
+class ByteBufferReadableFSDataInputStreamTest {
+
+    /**
+     * A stream taken from (or created for) the pool that fails while reading 
is neither returned to
+     * the pool nor closed by the caller, so {@code readFully} has to close it 
itself — otherwise
+     * the underlying (remote) stream leaks.
+     */
+    @Test
+    void testPooledStreamIsClosedWhenPositionedReadFails() throws Exception {
+        List<FailingStream> created = new ArrayList<>();
+        ByteBufferReadableFSDataInputStream stream =
+                new ByteBufferReadableFSDataInputStream(
+                        () -> {
+                            // The first stream is the sequential "original" 
stream of the wrapper;
+                            // every further one is created for the 
positioned-read pool.
+                            FailingStream s = new 
FailingStream(!created.isEmpty());
+                            created.add(s);
+                            return s;
+                        },
+                        4,
+                        1024);
+
+        assertThatThrownBy(() -> stream.readFully(10, ByteBuffer.allocate(8)))
+                .isInstanceOf(IOException.class)
+                .hasMessage("read failed");
+
+        assertThat(created).hasSize(2);
+        assertThat(created.get(1).closed).as("failed pooled stream must be 
closed").isTrue();
+        assertThat(created.get(0).closed).as("the original stream is 
untouched").isFalse();
+        stream.close();
+    }
+
+    private static final class FailingStream extends FSDataInputStream {
+        private final boolean failOnRead;
+        private long pos;
+        volatile boolean closed;
+
+        FailingStream(boolean failOnRead) {
+            this.failOnRead = failOnRead;
+        }
+
+        @Override
+        public void seek(long desired) {
+            pos = desired;
+        }
+
+        @Override
+        public long getPos() {
+            return pos;
+        }
+
+        @Override
+        public int read() throws IOException {
+            if (failOnRead) {
+                throw new IOException("read failed");
+            }
+            pos++;
+            return 0;
+        }
+
+        @Override
+        public void close() {
+            closed = true;
+        }
+    }
+}
diff --git 
a/flink-state-backends/flink-statebackend-forst/src/test/java/org/apache/flink/state/forst/fs/cache/CachedDataInputStreamTest.java
 
b/flink-state-backends/flink-statebackend-forst/src/test/java/org/apache/flink/state/forst/fs/cache/CachedDataInputStreamTest.java
new file mode 100644
index 00000000000..9e25ef92af0
--- /dev/null
+++ 
b/flink-state-backends/flink-statebackend-forst/src/test/java/org/apache/flink/state/forst/fs/cache/CachedDataInputStreamTest.java
@@ -0,0 +1,154 @@
+/*
+ * 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.flink.state.forst.fs.cache;
+
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.core.fs.FSDataInputStream;
+import org.apache.flink.core.fs.FileSystem;
+import org.apache.flink.core.fs.Path;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.IOException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/** Tests for the stream life cycle of {@link CachedDataInputStream}. */
+class CachedDataInputStreamTest {
+
+    @TempDir java.nio.file.Path tempDir;
+
+    private FileBasedCache newCache() {
+        return new FileBasedCache(
+                new Configuration(),
+                new SizeBasedCacheLimitPolicy(1024 * 1024, 64 * 1024 * 1024),
+                FileSystem.getLocalFileSystem(),
+                new Path(tempDir.toString(), "cache"),
+                null);
+    }
+
+    private FileCacheEntry newEntry(FileBasedCache cache) {
+        return new FileCacheEntry(
+                cache,
+                new Path(tempDir.toString(), "remote/1.sst"),
+                new Path(tempDir.toString(), "cache/1.sst"),
+                16);
+    }
+
+    /**
+     * The original (remote) stream is handed over to the cached stream by 
{@link
+     * FileCacheEntry#open} and nothing else references it, so closing the 
cached stream must close
+     * it — otherwise it leaks whatever the remote file system holds for it 
(for S3A: a leased
+     * connection of the HTTP connection pool).
+     */
+    @Test
+    void testCloseClosesOriginalStream() throws Exception {
+        try (FileBasedCache cache = newCache()) {
+            FileCacheEntry entry = newEntry(cache);
+            TrackingStream original = new TrackingStream();
+
+            // The entry is not loaded, so the stream is opened on the 
original stream only.
+            CachedDataInputStream stream = entry.open(original);
+            assertThat(original.closed).isFalse();
+
+            stream.close();
+            assertThat(original.closed).as("original stream must be 
closed").isTrue();
+
+            // Idempotent.
+            stream.close();
+            assertThat(original.closeCalls).isEqualTo(1);
+        }
+    }
+
+    @Test
+    void testCloseClosesCachedAndOriginalStream() throws Exception {
+        try (FileBasedCache cache = newCache()) {
+            FileCacheEntry entry = newEntry(cache);
+            TrackingStream cached = new TrackingStream();
+            TrackingStream original = new TrackingStream();
+
+            CachedDataInputStream stream =
+                    new CachedDataInputStream(cache, entry, cached, original);
+            stream.close();
+
+            assertThat(cached.closed).isTrue();
+            assertThat(original.closed).isTrue();
+        }
+    }
+
+    @Test
+    void testOriginalStreamIsClosedEvenIfCachedStreamFailsToClose() throws 
Exception {
+        try (FileBasedCache cache = newCache()) {
+            FileCacheEntry entry = newEntry(cache);
+            TrackingStream cached = new TrackingStream();
+            cached.failOnClose = new IOException("cached close failed");
+            TrackingStream original = new TrackingStream();
+            original.failOnClose = new IOException("original close failed");
+
+            CachedDataInputStream stream =
+                    new CachedDataInputStream(cache, entry, cached, original);
+
+            assertThatThrownBy(stream::close)
+                    .isInstanceOf(IOException.class)
+                    .hasMessage("cached close failed")
+                    .satisfies(
+                            e ->
+                                    assertThat(e.getSuppressed())
+                                            .extracting(Throwable::getMessage)
+                                            .containsExactly("original close 
failed"));
+            assertThat(cached.closeCalls).isEqualTo(1);
+            assertThat(original.closeCalls).as("original stream must still be 
closed").isEqualTo(1);
+        }
+    }
+
+    /** An in-memory {@link FSDataInputStream} that records whether it has 
been closed. */
+    static final class TrackingStream extends FSDataInputStream {
+        private long pos;
+        volatile boolean closed;
+        int closeCalls;
+        IOException failOnClose;
+
+        @Override
+        public void seek(long desired) {
+            pos = desired;
+        }
+
+        @Override
+        public long getPos() {
+            return pos;
+        }
+
+        @Override
+        public int read() {
+            pos++;
+            return 0;
+        }
+
+        @Override
+        public void close() throws IOException {
+            closeCalls++;
+            closed = true;
+            if (failOnClose != null) {
+                throw failOnClose;
+            }
+        }
+    }
+}

Reply via email to