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 89265005f9d [FLINK-40645][state/forst] Unregister closed streams from
FileCacheEntry#openedStreams (#29169)
89265005f9d is described below
commit 89265005f9d834e9596f54efdb82ab9162cd49c0
Author: Seungjoo Choi <[email protected]>
AuthorDate: Wed Oct 7 21:50:35 2026 +0900
[FLINK-40645][state/forst] Unregister closed streams from
FileCacheEntry#openedStreams (#29169)
Generated-by: Claude Code (Claude Fable 5.1)
---
.../forst/fs/cache/CachedDataInputStream.java | 5 +
.../flink/state/forst/fs/cache/FileCacheEntry.java | 13 ++
.../state/forst/fs/cache/FileCacheEntryTest.java | 162 +++++++++++++++++++++
3 files changed, 180 insertions(+)
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 da070128f0e..c9bd59cc262 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,6 +306,11 @@ public class CachedDataInputStream extends
FSDataInputStream implements ByteBuff
return;
}
closed = true;
+ // FileCacheEntry#doRemoveFile only drops closed streams when the file
itself is evicted
+ // or deleted, so while a file stays cached every open/close cycle of
a stream on it would
+ // otherwise leave a closed stream (and the remote stream it wraps) on
the heap. Done
+ // before the closes below so that a failing close cannot skip it.
+ cacheEntry.unregisterStream(this);
// 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
diff --git
a/flink-state-backends/flink-statebackend-forst/src/main/java/org/apache/flink/state/forst/fs/cache/FileCacheEntry.java
b/flink-state-backends/flink-statebackend-forst/src/main/java/org/apache/flink/state/forst/fs/cache/FileCacheEntry.java
index 4b1e9c70672..00e56d273a2 100644
---
a/flink-state-backends/flink-statebackend-forst/src/main/java/org/apache/flink/state/forst/fs/cache/FileCacheEntry.java
+++
b/flink-state-backends/flink-statebackend-forst/src/main/java/org/apache/flink/state/forst/fs/cache/FileCacheEntry.java
@@ -147,6 +147,19 @@ public final class FileCacheEntry extends
ReferenceCounted<Object> {
}
}
+ /**
+ * Removes a closed stream from the queue of opened streams, so that it
does not stay there
+ * until the file is evicted or deleted.
+ *
+ * @param stream the closed stream to unregister
+ */
+ void unregisterStream(CachedDataInputStream stream) {
+ // Deliberately not synchronized: LinkedBlockingQueue#remove is safe
against the concurrent
+ // iteration in doRemoveFile(), while taking the entry lock would
serialize every stream
+ // close against cache eviction.
+ openedStreams.remove(stream);
+ }
+
/**
* Retrieves the cached input stream for this cache entry if it is
available and the entry is in
* a valid state. The method attempts to open the cached stream if the
entry is in the LOADED
diff --git
a/flink-state-backends/flink-statebackend-forst/src/test/java/org/apache/flink/state/forst/fs/cache/FileCacheEntryTest.java
b/flink-state-backends/flink-statebackend-forst/src/test/java/org/apache/flink/state/forst/fs/cache/FileCacheEntryTest.java
new file mode 100644
index 00000000000..c4baa8b3b8e
--- /dev/null
+++
b/flink-state-backends/flink-statebackend-forst/src/test/java/org/apache/flink/state/forst/fs/cache/FileCacheEntryTest.java
@@ -0,0 +1,162 @@
+/*
+ * 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 bookkeeping of opened streams in {@link FileCacheEntry}. */
+class FileCacheEntryTest {
+
+ @TempDir java.nio.file.Path tempDir;
+
+ /**
+ * A closed stream must leave {@link FileCacheEntry#openedStreams}
immediately. Before, closed
+ * streams were only dropped by {@link FileCacheEntry#doRemoveFile()},
i.e. when the file was
+ * evicted or deleted, so they accumulated on the heap for as long as the
file stayed cached.
+ */
+ @Test
+ void testClosedStreamIsUnregisteredFromEntry() throws Exception {
+ FileBasedCache cache =
+ new FileBasedCache(
+ new Configuration(),
+ new SizeBasedCacheLimitPolicy(1024 * 1024, 64 * 1024 *
1024),
+ FileSystem.getLocalFileSystem(),
+ new Path(tempDir.toString(), "cache"),
+ null);
+ try {
+ FileCacheEntry entry =
+ new FileCacheEntry(
+ cache,
+ new Path(tempDir.toString(), "remote/1.sst"),
+ new Path(tempDir.toString(), "cache/1.sst"),
+ 16);
+
+ CachedDataInputStream first = entry.open(new NoopStream());
+ CachedDataInputStream second = entry.open(new NoopStream());
+ assertThat(entry.openedStreams).containsExactly(first, second);
+
+ first.close();
+ assertThat(entry.openedStreams)
+ .as("closed stream must not stay registered")
+ .containsExactly(second);
+
+ // Closing again must not fail or touch the registration of the
other stream.
+ first.close();
+ assertThat(entry.openedStreams).containsExactly(second);
+
+ second.close();
+ assertThat(entry.openedStreams).isEmpty();
+ } finally {
+ cache.close();
+ }
+ }
+
+ /**
+ * The wrapper is unregistered before the cached stream is closed, so a
failure while closing
+ * the cached stream must not leave the (now unusable) wrapper behind in
{@link
+ * FileCacheEntry#openedStreams}. A real cached stream from {@link
FileCacheEntry#open} never
+ * fails to close, so the CACHED_OPEN stream is built directly with a
stream that throws.
+ */
+ @Test
+ void testClosedStreamIsUnregisteredWhenCachedCloseThrows() throws
Exception {
+ FileBasedCache cache =
+ new FileBasedCache(
+ new Configuration(),
+ new SizeBasedCacheLimitPolicy(1024 * 1024, 64 * 1024 *
1024),
+ FileSystem.getLocalFileSystem(),
+ new Path(tempDir.toString(), "cache"),
+ null);
+ try {
+ FileCacheEntry entry =
+ new FileCacheEntry(
+ cache,
+ new Path(tempDir.toString(), "remote/1.sst"),
+ new Path(tempDir.toString(), "cache/1.sst"),
+ 16);
+
+ // CACHED_OPEN stream (constructor with a cache stream); its
cached stream's close()
+ // throws, so close() propagates from closeCachedStream() ->
fsdis.close().
+ CachedDataInputStream wrapper =
+ new CachedDataInputStream(
+ cache, entry, new ThrowingOnCloseStream(), new
NoopStream());
+ entry.openedStreams.add(wrapper);
+ assertThat(entry.openedStreams).containsExactly(wrapper);
+
+ assertThatThrownBy(wrapper::close).isInstanceOf(IOException.class);
+
+ assertThat(entry.openedStreams)
+ .as("a failing cached-stream close must still unregister
the wrapper")
+ .doesNotContain(wrapper);
+ } finally {
+ cache.close();
+ }
+ }
+
+ private static final class NoopStream extends FSDataInputStream {
+ private long pos;
+
+ @Override
+ public void seek(long desired) {
+ pos = desired;
+ }
+
+ @Override
+ public long getPos() {
+ return pos;
+ }
+
+ @Override
+ public int read() {
+ pos++;
+ return 0;
+ }
+ }
+
+ /** A stream whose {@link #close()} always fails, to exercise the
failing-close path. */
+ private static final class ThrowingOnCloseStream extends FSDataInputStream
{
+ @Override
+ public void seek(long desired) {}
+
+ @Override
+ public long getPos() {
+ return 0;
+ }
+
+ @Override
+ public int read() {
+ return 0;
+ }
+
+ @Override
+ public void close() throws IOException {
+ throw new IOException("close failed");
+ }
+ }
+}