This is an automated email from the ASF dual-hosted git repository.
HeartSaVioR pushed a commit to branch branch-4.1
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/branch-4.1 by this push:
new 993357db9cf5 [SPARK-58932][4.2][SS] Close the accepted socket in
TransformWithStateInPySparkStateServer
993357db9cf5 is described below
commit 993357db9cf51c22758d60cfea1f5f7af2bbe568
Author: Jiwon Park <[email protected]>
AuthorDate: Thu Aug 27 14:07:27 2026 +0900
[SPARK-58932][4.2][SS] Close the accepted socket in
TransformWithStateInPySparkStateServer
### What changes were proposed in this pull request?
Backport of #58205 to branch-4.2: run the request loop of
`TransformWithStateInPySparkStateServer.run()` inside `Utils.tryWithResource`
so the accepted socket is always closed. This branch does not have SPARK-58977,
so unlike master the `accept()` call is unchanged.
### Why are the changes needed?
The accepted socket is never closed, leaking one file descriptor per task
until the executor exhausts the ephemeral port range
(`java.net.BindException`). See #58205 for details.
### Does this PR introduce _any_ user-facing change?
No.
### How was this patch tested?
The two tests added in #58205 are included;
`TransformWithStateInPySparkStateServerSuite` passes locally on this branch.
### Was this patch authored or co-authored using generative AI tooling?
Yes. Generated-by: Claude Opus 5
Closes #58308 from j1wonpark/SPARK-58932-4.2.
Authored-by: Jiwon Park <[email protected]>
Signed-off-by: Jungtaek Lim <[email protected]>
(cherry picked from commit 6cb521671b948d51bbcefe28f160dd508a05baad)
Signed-off-by: Jungtaek Lim <[email protected]>
---
.../TransformWithStateInPySparkStateServer.scala | 10 ++++++-
...ansformWithStateInPySparkStateServerSuite.scala | 32 +++++++++++++++++++++-
2 files changed, 40 insertions(+), 2 deletions(-)
diff --git
a/sql/core/src/main/scala/org/apache/spark/sql/execution/python/streaming/TransformWithStateInPySparkStateServer.scala
b/sql/core/src/main/scala/org/apache/spark/sql/execution/python/streaming/TransformWithStateInPySparkStateServer.scala
index 4fee6a6e71d3..f457ee981fbf 100644
---
a/sql/core/src/main/scala/org/apache/spark/sql/execution/python/streaming/TransformWithStateInPySparkStateServer.scala
+++
b/sql/core/src/main/scala/org/apache/spark/sql/execution/python/streaming/TransformWithStateInPySparkStateServer.scala
@@ -18,7 +18,7 @@
package org.apache.spark.sql.execution.python.streaming
import java.io.{BufferedInputStream, BufferedOutputStream, DataInputStream,
DataOutputStream, EOFException, InterruptedIOException}
-import java.nio.channels.{Channels, ClosedByInterruptException,
ServerSocketChannel}
+import java.nio.channels.{Channels, ClosedByInterruptException,
ServerSocketChannel, SocketChannel}
import java.time.Duration
import scala.collection.mutable
@@ -40,6 +40,7 @@ import
org.apache.spark.sql.execution.streaming.state.StateMessage.KeyAndValuePa
import
org.apache.spark.sql.execution.streaming.state.StateMessage.StateResponseWithListGet
import org.apache.spark.sql.streaming.{ListState, MapState, TTLConfig,
ValueState}
import org.apache.spark.sql.types.StructType
+import org.apache.spark.util.Utils
/**
* This class is used to handle the state requests from the Python side. It
runs on a separate
@@ -140,6 +141,13 @@ class TransformWithStateInPySparkStateServer(
def run(): Unit = {
val listeningSocket = stateServerSocket.accept()
+ // The task completion listener closes only the listening server socket,
and the
+ // request loop has several early returns, so the accepted connection is
closed
+ // through tryWithResource.
+ Utils.tryWithResource(listeningSocket)(serveRequests)
+ }
+
+ private def serveRequests(listeningSocket: SocketChannel): Unit = {
// SPARK-51667: We have a pattern of sending messages continuously from
one side
// (Python -> JVM, and vice versa) before getting response from other
side. Since most
// messages we are sending are small, this triggers the bad combination of
Nagle's algorithm
diff --git
a/sql/core/src/test/scala/org/apache/spark/sql/execution/python/streaming/TransformWithStateInPySparkStateServerSuite.scala
b/sql/core/src/test/scala/org/apache/spark/sql/execution/python/streaming/TransformWithStateInPySparkStateServerSuite.scala
index e253a6aa45c3..d6a6a5609288 100644
---
a/sql/core/src/test/scala/org/apache/spark/sql/execution/python/streaming/TransformWithStateInPySparkStateServerSuite.scala
+++
b/sql/core/src/test/scala/org/apache/spark/sql/execution/python/streaming/TransformWithStateInPySparkStateServerSuite.scala
@@ -17,7 +17,9 @@
package org.apache.spark.sql.execution.python.streaming
import java.io.DataOutputStream
-import java.nio.channels.ServerSocketChannel
+import java.net.Socket
+import java.nio.ByteBuffer
+import java.nio.channels.{ServerSocketChannel, SocketChannel}
import scala.collection.mutable
@@ -110,6 +112,34 @@ class TransformWithStateInPySparkStateServerSuite extends
SparkFunSuite with Bef
.thenReturn(Seq(getIntegerRow(1)))
}
+ test("run closes the accepted socket once the request loop ends") {
+ val acceptedSocket = mock(classOf[SocketChannel])
+ when(serverSocket.accept()).thenReturn(acceptedSocket)
+ when(acceptedSocket.socket()).thenReturn(mock(classOf[Socket]))
+ // Ends the request loop right away: this test is about the socket, not
the requests.
+ when(acceptedSocket.isConnected).thenReturn(false)
+
+ stateServer.run()
+
+ verify(acceptedSocket).close()
+ }
+
+ test("run closes the accepted socket when the client disconnects") {
+ val acceptedSocket = mock(classOf[SocketChannel])
+ when(serverSocket.accept()).thenReturn(acceptedSocket)
+ when(acceptedSocket.socket()).thenReturn(mock(classOf[Socket]))
+ when(acceptedSocket.isConnected).thenReturn(true)
+ // Channels.newInputStream synchronizes on this before reading.
+ when(acceptedSocket.blockingLock()).thenReturn(new Object)
+ when(acceptedSocket.isBlocking).thenReturn(true)
+ // No bytes ever arrive, so the read hits EOF and the loop returns early.
+ when(acceptedSocket.read(any(classOf[ByteBuffer]))).thenReturn(-1)
+
+ stateServer.run()
+
+ verify(acceptedSocket).close()
+ }
+
test("set handle state") {
val message = StatefulProcessorCall.newBuilder().setSetHandleState(
SetHandleState.newBuilder().setState(HandleState.CREATED).build()).build()
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]