ericm-db commented on code in PR #58367:
URL: https://github.com/apache/spark/pull/58367#discussion_r4223834312


##########
python/pyspark/sql/connect/local_server_pool.py:
##########
@@ -680,7 +849,143 @@ def reap(self, uid: str) -> bool:
         states = self._directory.states(uid)
         if had_retired and "retired" in states:
             self._reap_retired(uid, states["retired"])
-        return not self._directory.states(uid)
+
+        remaining = self._directory.states(uid)
+        if set(remaining) == {"member"}:
+            # Nothing references the member directory anymore. The age gate 
keeps the logs
+            # of a freshly failed launch around long enough to be looked at.
+            try:
+                expired = (
+                    time.time() - os.path.getmtime(remaining["member"])
+                    > self._MEMBER_DIR_GC_AGE_SECONDS
+                )
+            except FileNotFoundError:
+                expired = True
+            if expired:
+                self._directory.remove_member_dir(uid)
+                remaining = self._directory.states(uid)
+        return not remaining
+
+    def _reap_pending(self, uid: str, path: str) -> None:
+        """Stop a verified abandoned launch, then withdraw its bookkeeping. A 
launch group
+        whose owner cannot be verified is kept only until the recovery 
deadline."""
+        data = self._directory.read_json(path)
+        pending = PendingState.from_data(data)
+        parsed_pid = pending.attendant_pid if pending is not None else None
+        created = pending.created if pending is not None else None
+        if pending is None and data is not None:
+            # Preserve independently valid lifecycle fields when another field 
is corrupt.
+            parsed_pid = PendingState.attendant_pid_from_data(data)
+            created = PendingState.created_from_data(data)
+        now = time.time()
+        if created is not None and created <= now:
+            age = now - created
+        else:
+            # Malformed or future timestamps are expired immediately, but the 
file's age
+            # still advances their give-up clock if inspection keeps failing.
+            try:
+                file_age = now - os.path.getmtime(path)
+            except OSError:
+                file_age = self._RETIRE_GIVE_UP_AFTER_SECONDS + 1
+            if file_age < 0:
+                file_age = self._RETIRE_GIVE_UP_AFTER_SECONDS + 1
+            age = self._LAUNCH_TIMEOUT_SECONDS + 1 + file_age

Review Comment:
   Fixed in 56b3129b655: future created timestamps fall back to file mtime, 
with a future mtime clamped to age zero. Added the backward-clock-step 
regression.



##########
python/pyspark/sql/connect/local_server_pool.py:
##########
@@ -680,7 +814,89 @@ def reap(self, uid: str) -> bool:
         states = self._directory.states(uid)
         if had_retired and "retired" in states:
             self._reap_retired(uid, states["retired"])
-        return not self._directory.states(uid)
+
+        remaining = self._directory.states(uid)
+        if set(remaining) == {"member"}:
+            # Nothing references the member directory anymore. The age gate 
keeps the logs
+            # of a freshly failed launch around long enough to be looked at.
+            try:
+                expired = (
+                    time.time() - os.path.getmtime(remaining["member"])
+                    > self._MEMBER_DIR_GC_AGE_SECONDS
+                )
+            except FileNotFoundError:
+                expired = True
+            if expired:
+                self._directory.remove_member_dir(uid)
+                remaining = self._directory.states(uid)
+        return not remaining
+
+    def _reap_pending(self, uid: str, path: str) -> None:
+        """A launch whose attendant died or hung: kill the attendant and 
whatever server
+        spark-daemon.sh may have recorded for it, and withdraw the launch's 
bookkeeping so
+        refills stop counting it."""
+        data = self._directory.read_json(path)
+        pending = PendingState.from_data(data)
+        parsed_pid = pending.attendant_pid if pending is not None else None
+        created = pending.created if pending is not None else None
+        if pending is None and data is not None:
+            # Preserve independently valid lifecycle fields when another field 
is corrupt.
+            parsed_pid = PendingState.attendant_pid_from_data(data)
+            created = PendingState.created_from_data(data)
+        age = time.time() - created if created is not None else 
self._LAUNCH_TIMEOUT_SECONDS + 1
+        attendant_pid = parsed_pid if parsed_pid is not None else -1
+        attendant_alive = _pid_alive(attendant_pid)

Review Comment:
   Fixed in 56b3129b655: the macOS zombie ps probe is confined to attendant 
liveness checks, and the liveness/docstring descriptions now match. General 
server and reuse probes no longer run ps on each call.



##########
python/pyspark/sql/connect/local_server_pool.py:
##########
@@ -680,7 +814,89 @@ def reap(self, uid: str) -> bool:
         states = self._directory.states(uid)
         if had_retired and "retired" in states:
             self._reap_retired(uid, states["retired"])
-        return not self._directory.states(uid)
+
+        remaining = self._directory.states(uid)
+        if set(remaining) == {"member"}:
+            # Nothing references the member directory anymore. The age gate 
keeps the logs
+            # of a freshly failed launch around long enough to be looked at.
+            try:
+                expired = (
+                    time.time() - os.path.getmtime(remaining["member"])
+                    > self._MEMBER_DIR_GC_AGE_SECONDS
+                )
+            except FileNotFoundError:
+                expired = True
+            if expired:
+                self._directory.remove_member_dir(uid)
+                remaining = self._directory.states(uid)
+        return not remaining
+
+    def _reap_pending(self, uid: str, path: str) -> None:
+        """A launch whose attendant died or hung: kill the attendant and 
whatever server
+        spark-daemon.sh may have recorded for it, and withdraw the launch's 
bookkeeping so
+        refills stop counting it."""
+        data = self._directory.read_json(path)
+        pending = PendingState.from_data(data)
+        parsed_pid = pending.attendant_pid if pending is not None else None
+        created = pending.created if pending is not None else None
+        if pending is None and data is not None:
+            # Preserve independently valid lifecycle fields when another field 
is corrupt.
+            parsed_pid = PendingState.attendant_pid_from_data(data)
+            created = PendingState.created_from_data(data)
+        age = time.time() - created if created is not None else 
self._LAUNCH_TIMEOUT_SECONDS + 1
+        attendant_pid = parsed_pid if parsed_pid is not None else -1
+        attendant_alive = _pid_alive(attendant_pid)
+        if not attendant_alive:
+            # Recovered fields from malformed state guide cleanup, but cannot 
authorize a
+            # process-group signal because the complete pending record was not 
validated.
+            if pending is not None and not self._signal_attendant_group(
+                attendant_pid, signal.SIGKILL, leader_may_be_dead=True
+            ):
+                # Keep the only launch-group handle when signalling failed but 
descendants
+                # remain. If the pid was reused by a live process, withdraw 
the stale record
+                # without signalling it.
+                if not _pid_alive(attendant_pid) and 
self._attendant_group_alive(attendant_pid):
+                    return
+            self.abort_launch(uid)
+        elif age > self._LAUNCH_TIMEOUT_SECONDS:
+            is_attendant = self._is_pool_attendant(attendant_pid, uid)
+            if is_attendant is None:
+                return
+            if is_attendant and not 
self._signal_attendant_group(attendant_pid, signal.SIGKILL):
+                # Keep the record when an attendant that still appears live 
could not be
+                # stopped, or when its leader exited during the attempt but 
children remain.
+                if _pid_alive(attendant_pid) or 
self._attendant_group_alive(attendant_pid):
+                    return
+            self.abort_launch(uid)
+
+    def abort_launch(self, uid: str) -> None:
+        """Withdraw a failed launch and retire any server it started before 
failing."""
+        states = self._directory.states(uid)
+        pending_path = states.get("pending")
+        server_path = states.get("server")
+        if "retired" in states:
+            # A previous abort can die after retiring the server but before 
removing the pending
+            # marker. The retired record owns the server's process-generation 
identity; never
+            # replace it with the weaker attendant record on the recovery pass.
+            if pending_path is not None:
+                self._directory.remove(pending_path)
+            self._directory.remove(self._directory.conf_path(uid))
+            return
+        if server_path is not None:
+            data = self._directory.read_json(server_path)
+            server_pid, process_start_id = self._recover_server_handle(uid, 
data)
+        else:
+            server_pid = self._recorded_daemon_pid(uid)
+            process_start_id = None
+        retirement_source = server_path or pending_path
+        retired_source = False
+        if server_pid is not None and retirement_source is not None:
+            # Keep shutdown state so a half-started JVM that ignores SIGTERM 
is escalated.
+            self._retire(retirement_source, server_pid, process_start_id)

Review Comment:
   Fixed in 56b3129b655: self-abort now accepts the required attendant argv 
prefix plus --master and --fingerprint. Added a regression with the real 
trailing flags and a negative test for mismatched argv.



-- 
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