Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -137,9 +138,9 @@ class TransformWithStateInPySparkStateServer(
listTimerMapForTest
} else new mutable.HashMap[String, Iterator[Long]]()

def run(): Unit = {
val listeningSocket = stateServerSocket.accept()

// The task closes only the listening socket, and the loop below has several early
// returns, so the accepted connection is closed through tryWithResource.
def run(): Unit = Utils.tryWithResource(stateServerSocket.accept()) { listeningSocket =>
// 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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()
Expand Down