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

dheerajturaga 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 4a011a8a7ab Fix airflow celery stop crashing on stale PID file (#72817)
4a011a8a7ab is described below

commit 4a011a8a7ab6fab9783ff4d27f328c97ad7aacba
Author: Y-C <[email protected]>
AuthorDate: Wed Sep 16 01:50:29 2026 +0800

    Fix airflow celery stop crashing on stale PID file (#72817)
    
    * Fix airflow celery stop crashing on stale PID file
    
    When a Celery worker dies without cleaning up its PID file (SIGKILL,
    OOM, host reboot), `airflow celery stop` reads the dead PID, fails with
    an uncaught psutil.NoSuchProcess traceback, and never reaches the
    PID-file removal. Every subsequent `stop` fails the same way until the
    operator deletes the file by hand.
    
    The worker start path already treats NoSuchProcess as "stale PID file,
    clean it up" in check_if_pidfile_process_is_running; the stop path is
    the only reader that did not. Only NoSuchProcess is handled on purpose:
    AccessDenied means a live process the caller cannot signal, and its PID
    file must not be removed.
    
    * Apply batched suggestions from code review
    
    Co-authored-by: rjgoyln <[email protected]>
    
    ---------
    
    Co-authored-by: Eason09053360 
<[email protected]>
    Co-authored-by: Elad Kalif <[email protected]>
    Co-authored-by: rjgoyln <[email protected]>
---
 .../airflow/providers/celery/cli/celery_command.py |  9 +++--
 .../tests/unit/celery/cli/test_celery_command.py   | 38 +++++++++++++++-------
 2 files changed, 34 insertions(+), 13 deletions(-)

diff --git 
a/providers/celery/src/airflow/providers/celery/cli/celery_command.py 
b/providers/celery/src/airflow/providers/celery/cli/celery_command.py
index 830c5d42cc8..7a447534f25 100644
--- a/providers/celery/src/airflow/providers/celery/cli/celery_command.py
+++ b/providers/celery/src/airflow/providers/celery/cli/celery_command.py
@@ -383,8 +383,13 @@ def stop_worker(args):
 
     # Send SIGTERM
     if pid:
-        worker_process = psutil.Process(pid)
-        worker_process.terminate()
+        try:
+            worker_process = psutil.Process(pid)
+            worker_process.terminate()
+        except psutil.NoSuchProcess:
+            log.warning(
+                "Worker process with PID %s is not running, PID file %s is 
stale.", pid, pid_file_path
+            )
 
     # Remove pid file
     remove_existing_pidfile(pid_file_path)
diff --git a/providers/celery/tests/unit/celery/cli/test_celery_command.py 
b/providers/celery/tests/unit/celery/cli/test_celery_command.py
index 9b9f4bcd7fd..b305993f657 100644
--- a/providers/celery/tests/unit/celery/cli/test_celery_command.py
+++ b/providers/celery/tests/unit/celery/cli/test_celery_command.py
@@ -27,6 +27,7 @@ from io import StringIO
 from unittest import mock
 from unittest.mock import MagicMock, patch
 
+import psutil
 import pytest
 
 from airflow.cli import cli_parser
@@ -73,23 +74,38 @@ class TestCeleryStopCommand:
             importlib.reload(cli_parser)
             cls.parser = cli_parser.get_parser()
 
-    @mock.patch("airflow.providers.celery.cli.celery_command.setup_locations")
-    @mock.patch("airflow.providers.celery.cli.celery_command.psutil.Process")
-    def test_if_right_pid_is_read(self, mock_process, mock_setup_locations, 
tmp_path):
+    @pytest.mark.parametrize(
+        ("process_side_effect", "terminate_side_effect", 
"expected_terminate_calls"),
+        [
+            pytest.param(None, None, 1, id="worker-running"),
+            pytest.param(psutil.NoSuchProcess(123), None, 0, 
id="stale-pid-file"),
+            pytest.param(None, psutil.NoSuchProcess(123), 1, 
id="worker-exits-before-terminate"),
+        ],
+    )
+    @mock.patch("airflow.providers.celery.cli.celery_command.setup_locations", 
autospec=True)
+    @mock.patch("airflow.providers.celery.cli.celery_command.psutil.Process", 
autospec=True)
+    def test_stop_worker_handles_missing_process(
+        self,
+        mock_process,
+        mock_setup_locations,
+        process_side_effect,
+        terminate_side_effect,
+        expected_terminate_calls,
+        tmp_path,
+    ):
         args = self.parser.parse_args(["celery", "stop"])
-        pid = "123"
+        pid = 123
         path = tmp_path / "testfile"
-        # Create pid file
-        path.write_text(pid)
-        # Setup mock
+        path.write_text(str(pid))
         mock_setup_locations.return_value = (os.fspath(path), None, None, None)
+        mock_process.side_effect = process_side_effect
+        mock_process.return_value.terminate.side_effect = terminate_side_effect
 
-        # Calling stop_worker should delete the temporary pid file
         celery_command.stop_worker(args)
-        # Check if works as expected
+
         assert not path.exists()
-        mock_process.assert_called_once_with(int(pid))
-        mock_process.return_value.terminate.assert_called_once_with()
+        mock_process.assert_called_once_with(pid)
+        assert mock_process.return_value.terminate.call_args_list == 
[mock.call()] * expected_terminate_calls
 
     
@mock.patch("airflow.providers.celery.cli.celery_command.read_pid_from_pidfile")
     @mock.patch("airflow.providers.celery.executors.celery_executor.app")

Reply via email to