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"}' )