From 0e1e4eeb004aefbcca0767169a6165292799523a Mon Sep 17 00:00:00 2001 From: Brian Axelson <86568017+baxeaz@users.noreply.github.com> Date: Mon, 2 Nov 2026 22:59:11 +0000 Subject: [PATCH] fix: log named pipe disconnects and timeouts in FrontendRunner requests On Windows, FrontendRunner._send_request logged "Failed to send {path} request" only for pywintypes.error. NamedPipeHelper raises its own PipeDisconnectedException and NamedPipeTimeoutError, which do not derive from pywintypes.error, so a broken pipe or read timeout during a heartbeat propagated without saying which request failed. Catch those exceptions as well, and include the elapsed time so the log shows whether the backend closed the connection at once or after its own request read timeout. Signed-off-by: Brian Axelson <86568017+baxeaz@users.noreply.github.com> --- .../_background/frontend_runner.py | 14 ++++++++--- .../unit/background/test_frontend_runner.py | 23 ++++++++++++++++--- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/src/openjd/adaptor_runtime/_background/frontend_runner.py b/src/openjd/adaptor_runtime/_background/frontend_runner.py index d9bd8644..f012acfe 100644 --- a/src/openjd/adaptor_runtime/_background/frontend_runner.py +++ b/src/openjd/adaptor_runtime/_background/frontend_runner.py @@ -37,7 +37,11 @@ _FRONTEND_RUNNER_REQUEST_TIMEOUT: float = 5.0 if OSName.is_windows(): - from ...adaptor_runtime_client.named_pipe.named_pipe_helper import NamedPipeHelper + from ...adaptor_runtime_client.named_pipe.named_pipe_helper import ( + NamedPipeHelper, + NamedPipeTimeoutError, + PipeDisconnectedException, + ) import pywintypes _logger = logging.getLogger(__name__) @@ -335,6 +339,7 @@ def _send_request( # This is used for aligning to the Linux's behavior in order to reuse the code in handler. # In linux, query string params will always be put in a list. params = {key: [value] for key, value in params.items()} + start = time.monotonic() try: response = NamedPipeHelper.send_named_pipe_request( self.connection_settings.socket, @@ -349,8 +354,11 @@ def _send_request( errmsg = f"Received unexpected HTTP status code {status}: {response['body']}" _logger.error(errmsg) raise HTTPError(response, errmsg) - except pywintypes.error as e: - _logger.error(f"Failed to send {path} request: {e}") + # send_named_pipe_request raises its own types for disconnects and timeouts. The elapsed + # time shows whether the backend closed the pipe at once or after its read timeout. + except (pywintypes.error, PipeDisconnectedException, NamedPipeTimeoutError) as e: + elapsed = time.monotonic() - start + _logger.error(f"Failed to send {path} request after {elapsed:.2f}s: {e}") raise return response else: # pragma: is-windows diff --git a/test/openjd/adaptor_runtime/unit/background/test_frontend_runner.py b/test/openjd/adaptor_runtime/unit/background/test_frontend_runner.py index 64736f0a..23f932fa 100644 --- a/test/openjd/adaptor_runtime/unit/background/test_frontend_runner.py +++ b/test/openjd/adaptor_runtime/unit/background/test_frontend_runner.py @@ -812,8 +812,12 @@ def test_sends_request( mock_read_from_pipe.assert_called_once() assert response == json.loads(mock_response) + @pytest.mark.parametrize( + "error_kind", ["pywintypes-error", "pipe-disconnected", "read-timeout"] + ) def test_raises_when_request_fails( self, + error_kind: str, mock_read_from_pipe: MagicMock, mock_response: str, runner: FrontendRunner, @@ -821,8 +825,18 @@ def test_raises_when_request_fails( ): # GIVEN import pywintypes + from openjd.adaptor_runtime_client.named_pipe.named_pipe_helper import ( + NamedPipeReadTimeoutError, + PipeDisconnectedException, + ) - error_instance = pywintypes.error(1, "FunctionName", "An error message") + error_instance = { + "pywintypes-error": pywintypes.error(1, "FunctionName", "An error message"), + "pipe-disconnected": PipeDisconnectedException( + pywintypes.error(109, "ReadFile", "The pipe has been ended.") + ), + "read-timeout": NamedPipeReadTimeoutError(5.0), + }[error_kind] mock_read_from_pipe.side_effect = error_instance method = "GET" path = "/path" @@ -834,12 +848,15 @@ def test_raises_when_request_fails( with patch.object( frontend_runner.NamedPipeHelper, "establish_named_pipe_connection" ) as mock_establish_named_pipe_connection: - with pytest.raises(pywintypes.error) as raised_exc: + with pytest.raises(type(error_instance)) as raised_exc: runner._send_request(method, path) # THEN assert raised_exc.value is error_instance - assert f"Failed to send {path} request: " in caplog.text + assert re.search( + rf"Failed to send {path} request after \d+\.\d\ds: {re.escape(str(error_instance))}", + caplog.text, + ) mock_write_to_pipe.assert_called_once_with( mock_establish_named_pipe_connection(), '{"method": "GET", "path": "/path"}' )