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

potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new c04b93baaae Keep SSH remote job running when its PTY session hangs up 
(#70573)
c04b93baaae is described below

commit c04b93baaaebd7025e55b53a5bc8352a70ddbc1a
Author: Jason(Zhe-You) Liu <[email protected]>
AuthorDate: Tue Jul 28 15:33:45 2026 +0800

    Keep SSH remote job running when its PTY session hangs up (#70573)
    
    SSHRemoteJobOperator launches the remote job detached under setsid so it
    survives the SSH connection dropping. The launcher redirected the job's 
stdout
    and stderr to /dev/null but left its stdin on the launching terminal. A 
fresh
    setsid session leader that still holds a terminal on any file descriptor
    re-adopts it as its controlling terminal, so when an SSH session that 
allocated
    a PTY hangs up, the job received SIGHUP and died -- orphaning the work the
    operator exists to keep alive. Detaching stdin as well leaves the job in a
    session with no controlling terminal, immune to the hangup.
---
 providers/ssh/src/airflow/providers/ssh/utils/remote_job.py |  8 ++++++--
 providers/ssh/tests/unit/ssh/utils/test_remote_job.py       | 10 +++-------
 2 files changed, 9 insertions(+), 9 deletions(-)

diff --git a/providers/ssh/src/airflow/providers/ssh/utils/remote_job.py 
b/providers/ssh/src/airflow/providers/ssh/utils/remote_job.py
index 496b0179d89..d8d7d8ff3fd 100644
--- a/providers/ssh/src/airflow/providers/ssh/utils/remote_job.py
+++ b/providers/ssh/src/airflow/providers/ssh/utils/remote_job.py
@@ -223,10 +223,14 @@ mv "'"$exit_code_tmp"'" "'"$exit_code_file"'"
 exit 0
 '
 
+# Redirect stdin from /dev/null too, not just stdout/stderr: a fresh setsid 
session
+# leader that keeps the launching terminal on any fd re-acquires it as its 
controlling
+# terminal, so a hangup (SSH session with a PTY dropping) would SIGHUP the 
detached job
+# and defeat the whole point of running it in its own session.
 if command -v setsid >/dev/null 2>&1; then
-  setsid bash -c "$job_script" >/dev/null 2>&1 &
+  setsid bash -c "$job_script" </dev/null >/dev/null 2>&1 &
 else
-  nohup bash -c "$job_script" >/dev/null 2>&1 &
+  nohup bash -c "$job_script" </dev/null >/dev/null 2>&1 &
 fi
 echo "{paths.job_id}"
 """
diff --git a/providers/ssh/tests/unit/ssh/utils/test_remote_job.py 
b/providers/ssh/tests/unit/ssh/utils/test_remote_job.py
index be93502057e..53c1f6f80b4 100644
--- a/providers/ssh/tests/unit/ssh/utils/test_remote_job.py
+++ b/providers/ssh/tests/unit/ssh/utils/test_remote_job.py
@@ -397,11 +397,6 @@ class TestPosixKillBehaviour:
         pgid = self._await_recorded_pid(paths)
         self._assert_kill_tears_down(paths, pgid, marker)
 
-    # Same environment-dependent process-group race that #69384 added reruns 
for on the
-    # sibling test; that marker was dropped in #69490 when this pty variant 
was written.
-    # The launch still depends on how the runner schedules the setsid fork, so 
keep main
-    # green on a fresh draw - the first attempt's assertion text stays in the 
CI log.
-    @pytest.mark.flaky(reruns=5)
     def test_kill_terminates_whole_job_tree_under_job_control(self, tmp_path):
         """With job control on, setsid(1) forks and the launcher's ``$!`` 
would name the
         short-lived setsid parent, not the job -- the condition the old 
wrapper orphaned
@@ -414,8 +409,9 @@ class TestPosixKillBehaviour:
         self._run_bash_mc_under_pty(
             wrapper + "\necho SUBMIT_DONE\n",
             b"SUBMIT_DONE",
-            # The job records its pid only after setsid(2) has put it in its 
own session, so
-            # a non-empty pid file is proof the pty hangup below can no longer 
reach it.
+            # Hang up only once the job is up (pid file written), so the pgrep 
below sees a
+            # started job. The job survives the hangup regardless: the wrapper 
detaches its
+            # stdin from the terminal, so the setsid session never adopts the 
pty.
             detached=lambda: pid_path.exists() and 
bool(pid_path.read_text().strip()),
         )
         pgid = self._await_recorded_pid(paths)

Reply via email to