This is an automated email from the ASF dual-hosted git repository.
shahar1 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 f8c7ead5b4a Task SDK: send error response on IPC decode failure
(#76555)
f8c7ead5b4a is described below
commit f8c7ead5b4a3af36c9c6706f122830a351cb7cc3
Author: Ayush Meel <[email protected]>
AuthorDate: Fri Nov 13 19:33:24 2026 +0530
Task SDK: send error response on IPC decode failure (#76555)
When handle_requests() fails to deserialize or validate an incoming request
frame, it logged the exception and issued continue. The task runner's recv(4)
call was left blocking with nothing ever coming back, deadlocking the child
process indefinitely. Meanwhile the supervisor kept heartbeating the API
server, so the scheduler's zombie detector never fired — the task would stay
stuck in RUNNING until someone killed it manually. Catching the exception and
replying with ErrorResponse(GEN [...]
---
.../src/airflow/sdk/execution_time/supervisor.py | 15 +++++++-
.../task_sdk/execution_time/test_supervisor.py | 42 ++++++++++++++++++++++
2 files changed, 56 insertions(+), 1 deletion(-)
diff --git a/task-sdk/src/airflow/sdk/execution_time/supervisor.py
b/task-sdk/src/airflow/sdk/execution_time/supervisor.py
index 4ab7889603b..6fd90a0785c 100644
--- a/task-sdk/src/airflow/sdk/execution_time/supervisor.py
+++ b/task-sdk/src/airflow/sdk/execution_time/supervisor.py
@@ -1179,8 +1179,21 @@ class WatchedSubprocess:
try:
msg =
self.decoder.validate_python(self._deserialize_request(request.body))
- except Exception:
+ except Exception as e:
log.exception("Unable to decode message", body=request.body)
+ with suppress(Exception):
+ self.send_msg(
+ msg=None,
+ error=ErrorResponse(
+ error=ErrorType.GENERIC_ERROR,
+ detail={
+ "status_code": None,
+ "message": f"Unable to decode message: {e}",
+ "exception_type": type(e).__name__,
+ },
+ ),
+ request_id=request.id,
+ )
continue
# Restore the task runner's trace context so that any outbound
HTTP calls made while
diff --git a/task-sdk/tests/task_sdk/execution_time/test_supervisor.py
b/task-sdk/tests/task_sdk/execution_time/test_supervisor.py
index 8e986595318..d9d1db6587d 100644
--- a/task-sdk/tests/task_sdk/execution_time/test_supervisor.py
+++ b/task-sdk/tests/task_sdk/execution_time/test_supervisor.py
@@ -4708,6 +4708,48 @@ class TestHandleRequest:
# Should not raise StopIteration (which would mean the loop crashed).
generator.send(req2)
+ @pytest.mark.parametrize(
+ "invalid_body",
+ [
+ pytest.param({"type": "NonExistentMessageType"},
id="unknown_message_type"),
+ pytest.param({"type": "GetVariable"}, id="missing_required_field"),
+ ],
+ )
+ def test_handle_requests_decoding_error_sends_error_response(
+ self, watched_subprocess, mocker, invalid_body
+ ):
+ """Test that invalid request frames produce an error response rather
than dropping the frame."""
+ watched_subprocess, read_socket = watched_subprocess
+
+ generator = watched_subprocess.handle_requests(log=mocker.Mock())
+ next(generator)
+
+ req_frame = _RequestFrame(id=randint(1, 2**32 - 1), body=invalid_body)
+ generator.send(req_frame)
+
+ read_socket.settimeout(0.5)
+ frame_len = int.from_bytes(read_socket.recv(4), "big")
+ bytes_ = read_socket.recv(frame_len)
+ frame = msgspec.msgpack.Decoder(_ResponseFrame).decode(bytes_)
+
+ assert frame.id == req_frame.id
+ assert frame.error is not None
+ assert frame.error["error"] == "GENERIC_ERROR"
+ assert "Unable to decode message" in frame.error["detail"]["message"]
+
+ comms = CommsDecoder(socket=None)
+ with pytest.raises(AirflowRuntimeError) as exc_info:
+ comms._from_frame(frame)
+
+ assert exc_info.value.error.error == ErrorType.GENERIC_ERROR
+ assert exc_info.value.error.detail["exception_type"] is not None
+
+ watched_subprocess.client.task_instances.succeed =
mocker.Mock(return_value=None)
+ msg = SucceedTask(end_date=timezone.parse("2024-10-31T12:00:00Z"))
+ req_valid = _RequestFrame(id=randint(1, 2**32 - 1),
body=msg.model_dump())
+ generator.send(req_valid)
+ watched_subprocess.client.task_instances.succeed.assert_called_once()
+
@pytest.mark.parametrize(
("msg", "api_method", "expected_state"),
[