venkata91 commented on code in PR #59277:
URL: https://github.com/apache/spark/pull/59277#discussion_r4213257706


##########
sql/connect/server/src/main/scala/org/apache/spark/sql/connect/service/SparkConnectSessionManager.scala:
##########
@@ -262,6 +262,12 @@ class SparkConnectSessionManager extends Logging {
 
   def closeSession(key: SessionKey, allowReconnect: Boolean = false): Unit = {
     val sessionHolder = removeSessionHolder(key, allowReconnect)
+    if (sessionHolder.isEmpty && allowReconnect) {
+      // The session is already gone (e.g. expired) and the client explicitly 
asked to be able to
+      // reconnect, accepting that its state is lost: drop the tombstone so 
the same session id
+      // can be recreated by getOrCreateIsolatedSession.
+      closedSessionsCache.invalidate(key)

Review Comment:
   Good point. Fixed: `removeSessionHolder` now always tombstones before 
removing the holder, and `shutdownSessionHolder` drops the tombstone only after 
`close()` completes. So nothing can recreate the key while key-based cleanup is 
still running. Added: `session id stays tombstoned until close completes when 
reconnect is allowed`.



##########
sql/connect/server/src/main/scala/org/apache/spark/sql/connect/service/SparkConnectSessionManager.scala:
##########
@@ -262,6 +262,12 @@ class SparkConnectSessionManager extends Logging {
 
   def closeSession(key: SessionKey, allowReconnect: Boolean = false): Unit = {
     val sessionHolder = removeSessionHolder(key, allowReconnect)
+    if (sessionHolder.isEmpty && allowReconnect) {
+      // The session is already gone (e.g. expired) and the client explicitly 
asked to be able to
+      // reconnect, accepting that its state is lost: drop the tombstone so 
the same session id
+      // can be recreated by getOrCreateIsolatedSession.
+      closedSessionsCache.invalidate(key)

Review Comment:
   Fixed. The after-the-fact `invalidate` is gone. `shutdownSessionHolder` is 
now the only place that decides: after `close()` it either `invalidate`s 
(reconnect allowed) or `put`s the final info, so there is no race between the 
two.



##########
python/pyspark/sql/connect/client/core.py:
##########
@@ -2182,10 +2182,22 @@ def interrupt_operation(self, op_id: str) -> 
Optional[List[str]]:
         except Exception as error:
             self._handle_error(error)
 
-    def release_session(self) -> None:
+    def release_session(self, allow_reconnect: bool = False) -> None:

Review Comment:
   Agreed. I removed the client-side `release_session(allow_reconnect)` change 
entirely. With the opt-in conf, no client call lifts the fence on a closed 
session anymore, so the old client can't touch the recreated one through this 
path.



##########
python/pyspark/sql/connect/client/core.py:
##########
@@ -2182,10 +2182,22 @@ def interrupt_operation(self, op_id: str) -> 
Optional[List[str]]:
         except Exception as error:
             self._handle_error(error)
 
-    def release_session(self) -> None:
+    def release_session(self, allow_reconnect: bool = False) -> None:
+        """
+        Release the session on the server.
+
+        Parameters
+        ----------
+        allow_reconnect : bool, optional
+            If True, the session id can be reused afterwards to create a new, 
empty session,
+            including when the session has already been closed on the server 
(for example,
+            after it expired). All server-side state of the old session 
(temporary views,
+            temporary functions, cached data, the current catalog) is lost.
+        """
         req = pb2.ReleaseSessionRequest()
         req.session_id = self._session_id
         req.client_type = self._builder.userAgent
+        req.allow_reconnect = allow_reconnect

Review Comment:
   Same as [3]: dropped the client changes. A client that opts in has to handle 
`SESSION_CHANGED` and start a fresh `SparkSession` anyway. That's also 
documented in the conf doc: only opt in if you don't depend on server-side 
state surviving a close.



##########
sql/connect/server/src/main/scala/org/apache/spark/sql/connect/service/SparkConnectSessionManager.scala:
##########
@@ -262,6 +262,12 @@ class SparkConnectSessionManager extends Logging {
 
   def closeSession(key: SessionKey, allowReconnect: Boolean = false): Unit = {
     val sessionHolder = removeSessionHolder(key, allowReconnect)
+    if (sessionHolder.isEmpty && allowReconnect) {

Review Comment:
   Makes sense. Reworked to your option (1): 
`spark.connect.session.allowReconnectAfterClose` is a session-scoped conf, 
default `false`. The session has to opt in while it is alive, and the decision 
is read from the holder at shutdown. Releases and expiries of sessions that 
didn't opt in keep the `SESSION_CLOSED` fence as before.



##########
sql/connect/server/src/test/scala/org/apache/spark/sql/connect/service/SparkConnectSessionManagerSuite.scala:
##########
@@ -159,6 +159,29 @@ class SparkConnectSessionManagerSuite extends 
SharedSparkSession {
     assert(closedSessionInfo.get.closedTimeMs.isDefined)
   }
 
+  test("releasing an already closed session with allowReconnect allows 
recreating it") {
+    val key = SessionKey("user", UUID.randomUUID().toString)
+    val sessionHolder = 
SparkConnectService.sessionManager.getOrCreateIsolatedSession(key, None)
+    
SparkConnectService.sessionManager.periodicMaintenance(defaultInactiveTimeoutMs 
= 0L)
+
+    val ex = intercept[SparkSQLException] {
+      SparkConnectService.sessionManager.getOrCreateIsolatedSession(key, None)
+    }
+    assert(ex.getCondition == "INVALID_HANDLE.SESSION_CLOSED")
+
+    // Without allowReconnect the tombstone stays.
+    SparkConnectService.sessionManager.closeSession(key)
+    intercept[SparkSQLException] {
+      SparkConnectService.sessionManager.getOrCreateIsolatedSession(key, None)
+    }
+
+    SparkConnectService.sessionManager.closeSession(key, allowReconnect = true)
+    val recreated = 
SparkConnectService.sessionManager.getOrCreateIsolatedSession(key, None)
+    assert(recreated.serverSessionId != sessionHolder.serverSessionId)

Review Comment:
   Done. Removed the tautological assert. Added a 
`SparkConnectReleaseSessionHandler` path test: `ReleaseSession honors 
allow_reconnect and the session opt-in`. The recreate test now checks that the 
new session starts from a clean conf instead.



##########
sql/connect/server/src/test/scala/org/apache/spark/sql/connect/service/SparkConnectSessionManagerSuite.scala:
##########
@@ -159,6 +159,29 @@ class SparkConnectSessionManagerSuite extends 
SharedSparkSession {
     assert(closedSessionInfo.get.closedTimeMs.isDefined)
   }
 
+  test("releasing an already closed session with allowReconnect allows 
recreating it") {
+    val key = SessionKey("user", UUID.randomUUID().toString)
+    val sessionHolder = 
SparkConnectService.sessionManager.getOrCreateIsolatedSession(key, None)
+    
SparkConnectService.sessionManager.periodicMaintenance(defaultInactiveTimeoutMs 
= 0L)
+
+    val ex = intercept[SparkSQLException] {
+      SparkConnectService.sessionManager.getOrCreateIsolatedSession(key, None)
+    }
+    assert(ex.getCondition == "INVALID_HANDLE.SESSION_CLOSED")
+
+    // Without allowReconnect the tombstone stays.
+    SparkConnectService.sessionManager.closeSession(key)

Review Comment:
   Done. All negative cases now go through a helper that asserts `getCondition 
== "INVALID_HANDLE.SESSION_CLOSED"`.



##########
sql/connect/server/src/main/scala/org/apache/spark/sql/connect/service/SparkConnectSessionManager.scala:
##########
@@ -262,6 +262,12 @@ class SparkConnectSessionManager extends Logging {
 
   def closeSession(key: SessionKey, allowReconnect: Boolean = false): Unit = {
     val sessionHolder = removeSessionHolder(key, allowReconnect)
+    if (sessionHolder.isEmpty && allowReconnect) {

Review Comment:
   Removed along with the after-the-fact invalidate.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to