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 498acc8cf26 Fix timeout_with_traceback crashes on Windows and non-main 
threads (#63664)
498acc8cf26 is described below

commit 498acc8cf269e3ec2717eec25483aac7bf344533
Author: Kumar Gautam <[email protected]>
AuthorDate: Thu Apr 2 23:08:53 2026 +0530

    Fix timeout_with_traceback crashes on Windows and non-main threads (#63664)
    
    * Fix timeout_with_traceback crashes on Windows and non-main threads
    
    * Add regression tests and newsfragment for timeout_with_traceback fix (fix 
lint)
    
    * Fix syntax in newsfragment for timeout_with_traceback
---
 airflow-core/newsfragments/63664.bugfix.rst        |  1 +
 airflow-core/src/airflow/utils/db.py               | 19 +++--
 airflow-core/tests/unit/utils/test_db_timeout.py   | 58 ++++++++++++++
 .../tests/unit/utils/test_timeout_traceback.py     | 88 ++++++++++++++++++++++
 4 files changed, 161 insertions(+), 5 deletions(-)

diff --git a/airflow-core/newsfragments/63664.bugfix.rst 
b/airflow-core/newsfragments/63664.bugfix.rst
new file mode 100644
index 00000000000..e759d608418
--- /dev/null
+++ b/airflow-core/newsfragments/63664.bugfix.rst
@@ -0,0 +1 @@
+Fix ``timeout_with_traceback`` crashes on Windows and non-main threads
diff --git a/airflow-core/src/airflow/utils/db.py 
b/airflow-core/src/airflow/utils/db.py
index a32644df803..13bbd440327 100644
--- a/airflow-core/src/airflow/utils/db.py
+++ b/airflow-core/src/airflow/utils/db.py
@@ -152,15 +152,24 @@ def timeout_with_traceback(seconds, message="Operation 
timed out"):
         raise TimeoutException(message)
 
     # Set the signal handler
-    old_handler = signal.signal(signal.SIGALRM, timeout_handler)
-    signal.alarm(seconds)
+    timeout_supported = False
+    try:
+        old_handler = signal.signal(signal.SIGALRM, timeout_handler)
+        signal.alarm(seconds)
+        timeout_supported = True
+    except (AttributeError, ValueError):
+        log.warning(
+            "timeout_with_traceback requires signal.SIGALRM and the main 
thread. "
+            "Proceeding without a timeout."
+        )
 
     try:
         yield
     finally:
-        # Cancel the alarm and restore the old handler
-        signal.alarm(0)
-        signal.signal(signal.SIGALRM, old_handler)
+        if timeout_supported:
+            # Cancel the alarm and restore the old handler
+            signal.alarm(0)
+            signal.signal(signal.SIGALRM, old_handler)
 
 
 @provide_session
diff --git a/airflow-core/tests/unit/utils/test_db_timeout.py 
b/airflow-core/tests/unit/utils/test_db_timeout.py
new file mode 100644
index 00000000000..62fc6591005
--- /dev/null
+++ b/airflow-core/tests/unit/utils/test_db_timeout.py
@@ -0,0 +1,58 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+from unittest import mock
+
+import pytest
+
+from airflow.utils.db import timeout_with_traceback
+
+
+class TestTimeoutWithTraceback:
+    def test_timeout_with_traceback_basic(self):
+        """Test that the context manager works normally on supported 
systems."""
+        with mock.patch("signal.signal") as mock_signal, 
mock.patch("signal.alarm") as mock_alarm:
+            mock_signal.return_value = "old_handler"
+
+            with timeout_with_traceback(10):
+                pass
+
+            # Check signal handler was set
+            mock_signal.assert_any_call(mock.ANY, mock.ANY)
+            mock_alarm.assert_any_call(10)
+
+            # Check cleanup happened
+            mock_alarm.assert_any_call(0)
+            mock_signal.assert_any_call(mock.ANY, "old_handler")
+
+    @pytest.mark.parametrize("exception", [AttributeError, ValueError])
+    def test_timeout_with_traceback_unsupported(self, exception, caplog):
+        """
+        Test that it handles missing SIGALRM or non-main thread gracefully.
+        AttributeError happens on Windows (missing SIGALRM).
+        ValueError happens in non-main threads (signal.signal not allowed).
+        """
+        # Patch BOTH signal.signal and signal.SIGALRM to be safe across 
platforms
+        with mock.patch("signal.signal", side_effect=exception), 
mock.patch("signal.SIGALRM", create=True):
+            with timeout_with_traceback(10):
+                # Should not raise exception
+                pass
+
+            assert "timeout_with_traceback requires signal.SIGALRM and the 
main thread" in caplog.text
+            assert "Proceeding without a timeout" in caplog.text
diff --git a/airflow-core/tests/unit/utils/test_timeout_traceback.py 
b/airflow-core/tests/unit/utils/test_timeout_traceback.py
new file mode 100644
index 00000000000..3faf6e98491
--- /dev/null
+++ b/airflow-core/tests/unit/utils/test_timeout_traceback.py
@@ -0,0 +1,88 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+import signal
+from unittest import mock
+
+import pytest
+
+from airflow.utils.db import timeout_with_traceback
+
+
+class TestTimeoutWithTraceback:
+    def test_timeout_supported_unix(self):
+        """Test that it works normally on Unix-like systems where SIGALRM is 
supported."""
+        if not hasattr(signal, "SIGALRM"):
+            pytest.skip("SIGALRM not supported on this platform")
+
+        with mock.patch("signal.signal") as mock_signal, 
mock.patch("signal.alarm") as mock_alarm:
+            mock_signal.return_value = "old_handler"
+
+            with timeout_with_traceback(seconds=5):
+                pass
+
+            mock_signal.assert_any_call(signal.SIGALRM, mock.ANY)
+            mock_alarm.assert_any_call(5)
+            # Cleanup
+            mock_alarm.assert_any_call(0)
+            mock_signal.assert_any_call(signal.SIGALRM, "old_handler")
+
+    @pytest.mark.parametrize(
+        "exception",
+        [
+            pytest.param(AttributeError("SIGALRM missing"), 
id="windows_attribute_error"),
+            pytest.param(ValueError("signal only works in main thread"), 
id="non_main_thread_value_error"),
+        ],
+    )
+    def test_timeout_unsupported_platforms_or_threads(self, exception):
+        """Test that it handles unsupported platforms (Windows) or non-main 
threads gracefully."""
+        # We need to patch signal.signal to raise the exception
+        # Even if SIGALRM exists, we force it to fail to test the catch block
+
+        with (
+            mock.patch("signal.signal", side_effect=exception),
+            mock.patch("airflow.utils.db.log") as mock_log,
+        ):
+            with timeout_with_traceback(seconds=5):
+                # Should not raise any exception
+                pass
+
+            mock_log.warning.assert_called_once_with(
+                "timeout_with_traceback requires signal.SIGALRM and the main 
thread. "
+                "Proceeding without a timeout."
+            )
+
+    def test_timeout_happens(self):
+        """Test that it actually raises TimeoutException when time is up."""
+        if not hasattr(signal, "SIGALRM"):
+            pytest.skip("SIGALRM not supported on this platform")
+
+        # We can't easily test real timeout in unit test without blocking
+        # but we can call the handler directly to see if it raises
+
+        with mock.patch("signal.signal") as mock_signal:
+            with timeout_with_traceback(seconds=1):
+                # The handler is the second argument to the first call of 
signal.signal
+                handler = mock_signal.call_args_list[0][0][1]
+
+                with pytest.raises(Exception, match="Operation timed out") as 
excinfo:
+                    handler(signal.SIGALRM, None)
+
+                assert "Operation timed out" in str(excinfo.value)
+                assert excinfo.type.__name__ == "TimeoutException"

Reply via email to