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

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


The following commit(s) were added to refs/heads/master by this push:
     new 62fd9cb420db [SPARK-57803][CORE] Delete the temp file when 
closeAndRead fails on the fetch-to-disk path
62fd9cb420db is described below

commit 62fd9cb420db48a3ff7edd0f18b41da6e6a0e3a5
Author: YangJie <[email protected]>
AuthorDate: Thu Jul 2 10:47:56 2026 +0800

    [SPARK-57803][CORE] Delete the temp file when closeAndRead fails on the 
fetch-to-disk path
    
    ### What changes were proposed in this pull request?
    
    `OneForOneBlockFetcher.DownloadCallback.onComplete` (the fetch-to-disk 
path) calls `channel.closeAndRead()` to close the download channel and obtain a 
`ManagedBuffer` over the temp file, then either registers the temp file for 
later cleanup or deletes it. On an `IOException` from `closeAndRead()` it 
rethrew without any cleanup, leaking the temp file (which is registered for 
cleanup only after a successful read) and possibly an open channel. The sibling 
`onFailure` already closes the c [...]
    
    This wraps `closeAndRead()` so a failure deletes the temp file and closes 
the channel before rethrowing, matching `onFailure`. The happy path is 
unchanged: the buffer is handed to the listener only after `closeAndRead()` 
succeeds.
    
    ### Why are the changes needed?
    
    The temp file is created on the fetching executor when a remote block 
exceeds `spark.maxRemoteBlockSizeFetchToMem` and is streamed to disk. Executors 
are long-lived, so the orphaned temp files accumulate under the local 
directories until the executor exits.
    
    ### Does this PR introduce _any_ user-facing change?
    
    No.
    
    ### How was this patch tested?
    
    New 
`OneForOneBlockFetcherSuite.testFetchToDiskClosesAndDeletesTempFileWhenCloseAndReadFails`
 stubs a `closeAndRead()` that throws and verifies the channel is closed, the 
temp file is deleted, and it is never registered for cleanup. It fails without 
the fix and passes with it. `build/sbt 'network-shuffle/testOnly 
*OneForOneBlockFetcherSuite'` -> 14 tests pass.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code
    
    Closes #56920 from LuciferYang/worktree-spark-fetch-tmpfile-leak.
    
    Authored-by: YangJie <[email protected]>
    Signed-off-by: yangjie01 <[email protected]>
---
 .../network/shuffle/OneForOneBlockFetcher.java     | 13 +++++-
 .../shuffle/OneForOneBlockFetcherSuite.java        | 50 ++++++++++++++++++++++
 2 files changed, 62 insertions(+), 1 deletion(-)

diff --git 
a/common/network-shuffle/src/main/java/org/apache/spark/network/shuffle/OneForOneBlockFetcher.java
 
b/common/network-shuffle/src/main/java/org/apache/spark/network/shuffle/OneForOneBlockFetcher.java
index c5c6ab313e19..5c98ae7a5b1e 100644
--- 
a/common/network-shuffle/src/main/java/org/apache/spark/network/shuffle/OneForOneBlockFetcher.java
+++ 
b/common/network-shuffle/src/main/java/org/apache/spark/network/shuffle/OneForOneBlockFetcher.java
@@ -364,7 +364,18 @@ public class OneForOneBlockFetcher {
 
     @Override
     public void onComplete(String streamId) throws IOException {
-      listener.onBlockFetchSuccess(blockIds[chunkIndex], 
channel.closeAndRead());
+      ManagedBuffer buffer;
+      try {
+        buffer = channel.closeAndRead();
+      } catch (IOException e) {
+        // closeAndRead() failed (typically from the channel close) before the 
buffer was handed
+        // off, so the temp file was never registered for cleanup. Delete it, 
and close the
+        // channel in case it was left open, mirroring onFailure, instead of 
leaking them.
+        channel.close();
+        targetFile.delete();
+        throw e;
+      }
+      listener.onBlockFetchSuccess(blockIds[chunkIndex], buffer);
       if (!downloadFileManager.registerTempFileToClean(targetFile)) {
         targetFile.delete();
       }
diff --git 
a/common/network-shuffle/src/test/java/org/apache/spark/network/shuffle/OneForOneBlockFetcherSuite.java
 
b/common/network-shuffle/src/test/java/org/apache/spark/network/shuffle/OneForOneBlockFetcherSuite.java
index f127568c8a33..9a58ff7eaee9 100644
--- 
a/common/network-shuffle/src/test/java/org/apache/spark/network/shuffle/OneForOneBlockFetcherSuite.java
+++ 
b/common/network-shuffle/src/test/java/org/apache/spark/network/shuffle/OneForOneBlockFetcherSuite.java
@@ -17,6 +17,7 @@
 
 package org.apache.spark.network.shuffle;
 
+import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.util.HashMap;
 import java.util.Iterator;
@@ -33,14 +34,17 @@ import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import org.apache.spark.network.buffer.ManagedBuffer;
 import org.apache.spark.network.buffer.NettyManagedBuffer;
 import org.apache.spark.network.buffer.NioManagedBuffer;
 import org.apache.spark.network.client.ChunkReceivedCallback;
 import org.apache.spark.network.client.RpcResponseCallback;
+import org.apache.spark.network.client.StreamCallback;
 import org.apache.spark.network.client.TransportClient;
 import org.apache.spark.network.shuffle.protocol.BlockTransferMessage;
 import org.apache.spark.network.shuffle.protocol.FetchShuffleBlocks;
@@ -283,6 +287,52 @@ public class OneForOneBlockFetcherSuite {
         new int[][] {{ 0 }}), conf));
   }
 
+  /**
+   * Verifies that when fetching to disk, a failure of
+   * {@link DownloadFileWritableChannel#closeAndRead} does not leak the temp 
file: the channel is
+   * closed and the temp file is deleted, and it is never registered for later 
cleanup.
+   */
+  @Test
+  public void testFetchToDiskClosesAndDeletesTempFileWhenCloseAndReadFails() 
throws IOException {
+    String blockId = "shuffle_0_0_0";
+    String[] blockIds = new String[] { blockId };
+
+    TransportClient client = mock(TransportClient.class);
+    BlockFetchingListener listener = mock(BlockFetchingListener.class);
+
+    DownloadFileWritableChannel channel = 
mock(DownloadFileWritableChannel.class);
+    when(channel.closeAndRead()).thenThrow(new IOException("boom"));
+    DownloadFile downloadFile = mock(DownloadFile.class);
+    when(downloadFile.openForWriting()).thenReturn(channel);
+    DownloadFileManager downloadFileManager = mock(DownloadFileManager.class);
+    when(downloadFileManager.createTempFile(any())).thenReturn(downloadFile);
+
+    OneForOneBlockFetcher fetcher = new OneForOneBlockFetcher(
+      client, "app-id", "exec-id", blockIds, listener, conf, 
downloadFileManager);
+
+    // Respond to the "openBlocks" RPC with a StreamHandle of a single chunk.
+    doAnswer(invocation -> {
+      RpcResponseCallback callback = (RpcResponseCallback) 
invocation.getArguments()[1];
+      callback.onSuccess(new StreamHandle(123, 1).toByteBuffer());
+      return null;
+    }).when(client).sendRpc(any(ByteBuffer.class), 
any(RpcResponseCallback.class));
+
+    // Drive the StreamCallback's onComplete, which triggers the failing 
closeAndRead().
+    doAnswer(invocation -> {
+      StreamCallback callback = (StreamCallback) invocation.getArguments()[1];
+      assertThrows(IOException.class, () -> callback.onComplete("stream"));
+      return null;
+    }).when(client).stream(any(), any(StreamCallback.class));
+
+    fetcher.start();
+
+    verify(channel).close();
+    verify(downloadFile).delete();
+    verify(downloadFileManager, never()).registerTempFileToClean(any());
+    // closeAndRead() failed, so the buffer must not have been handed off to 
the listener.
+    verify(listener, never()).onBlockFetchSuccess(any(), any());
+  }
+
   /**
    * Begins a fetch on the given set of blocks by mocking out the server side 
of the RPC which
    * simply returns the given (BlockId, Block) pairs.


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to