a2aproject / a2aproject/a2a-tck

[Bug]: REST transport streaming fails with "Event loop is closed" error

Abierto
#99 0 comentarios 1 reacción 0 asignados Ver en GitHub
Lenguaje dominante
Python
Estrellas
50
Forks
40
Merge medio
7 d 1 h
PR fusionados (30 d)
1

Descripción

### What happened?

The REST client's streaming methods (send_streaming_message and subscribe_to_task) fail intermittently with `RuntimeError: Event loop is closed during async cleanup.`

### Possible Root Cause

The REST client re-uses a shared [AsyncClient](https://github.com/a2aproject/a2a-tck/blob/main/tck/transport/rest_client.py#L290) for streaming. While the JSON-RPC client creates a [new client](https://github.com/a2aproject/a2a-tck/blob/main/tck/transport/jsonrpc_client.py#L189-L195) per request. When pytest-asyncio creates a new event loop for each test (default behavior: `asyncio_default_test_loop_scope=function`), the shared AsyncClient from a previous test can become bound to a closed event loop, causing the cleanup errors for the REST scenario.

### Possible Solutions

**Option 1:** Change pytest-asyncio loop scope. Add to [pyproject.toml](https://github.com/a2aproject/a2a-tck/blob/main/pyproject.toml#L67): `asyncio_default_test_loop_scope = "session"`
This shares a single event loop across all tests, keeping the shared client valid. However, this seems a workaround that will affects all tests as this option is global and may have unintended side effects. Don't know if it's the cleanest solution.

**Option 2:** Create new client per request
Update REST streaming methods to match JSON-RPC behavior

Before:
```python
async with self.async_client.stream("POST", url, json=payload, headers=headers) as response:
```

After:
```python
async with AsyncClient(timeout=self.timeout, verify=self._create_ssl_context()) as client:
async with client.stream("POST", url, json=payload, headers=headers) as response:
```

I let you analyze this issue that was discovered while working on this [PR](https://github.com/a2aproject/a2a-js/pull/142) in **a2a-js** sdk. See discussion [here](https://github.com/a2aproject/a2a-js/pull/142#discussion_r2565047768) for more details. LMK if you need further details to help you debug the situation. If analysis is valid and possible solutions seems acceptable to you, I'd rather go with **option 2**, but I let you decide, just a proposition.

### Relevant log output

```shell
====================================================================================================== FAILURES ======================================================================================================
__________________________________________________________________________________________ test_sse_event_format_compliance __________________________________________________________________________________________
tck/transport/rest_client.py:284: in send_streaming_message
async with self.async_client.stream("POST", url, json=payload, headers=headers) as response:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../../.local/share/uv/python/cpython-3.14.0-macos-aarch64-none/lib/python3.14/contextlib.py:214: in __aenter__
return await anext(self.gen)
^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14/site-packages/httpx/_client.py:1583: in stream
response = await self.send(
.venv/lib/python3.14/site-packages/httpx/_client.py:1629: in send
response = await self._send_handling_auth(
.venv/lib/python3.14/site-packages/httpx/_client.py:1657: in _send_handling_auth
response = await self._send_handling_redirects(
.venv/lib/python3.14/site-packages/httpx/_client.py:1694: in _send_handling_redirects
response = await self._send_single_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14/site-packages/httpx/_client.py:1730: in _send_single_request
response = await transport.handle_async_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14/site-packages/httpx/_transports/default.py:394: in handle_async_request
resp = await self._pool.handle_async_request(req)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14/site-packages/httpcore/_async/connection_pool.py:256: in handle_async_request
raise exc from None
.venv/lib/python3.14/site-packages/httpcore/_async/connection_pool.py:236: in handle_async_request
response = await connection.handle_async_request(
.venv/lib/python3.14/site-packages/httpcore/_async/connection.py:103: in handle_async_request
return await self._connection.handle_async_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.14/site-packages/httpcore/_async/http11.py:135: in handle_async_request
await self._response_closed()
.venv/lib/python3.14/site-packages/httpcore/_async/http11.py:250: in _response_closed
await self.aclose()
.venv/lib/python3.14/site-packages/httpcore/_async/http11.py:258: in aclose
await self._network_stream.aclose()
.venv/lib/python3.14/site-packages/httpcore/_backends/anyio.py:53: in aclose
await self._stream.aclose()
.venv/lib/python3.14/site-packages/anyio/_backends/_asyncio.py:1352: in aclose
self._transport.close()
../../../.local/share/uv/python/cpython-3.14.0-macos-aarch64-none/lib/python3.14/asyncio/selector_events.py:1209: in close
super().close()
../../../.local/share/uv/python/cpython-3.14.0-macos-aarch64-none/lib/python3.14/asyncio/selector_events.py:869: in close
self._loop.call_soon(self._call_connection_lost, None)
../../../.local/share/uv/python/cpython-3.14.0-macos-aarch64-none/lib/python3.14/asyncio/base_events.py:827: in call_soon
self._check_closed()
../../../.local/share/uv/python/cpython-3.14.0-macos-aarch64-none/lib/python3.14/asyncio/base_events.py:550: in _check_closed
raise RuntimeError('Event loop is closed')
E RuntimeError: Event loop is closed

During handling of the above exception, another exception occurred:
tests/optional/capabilities/test_streaming_methods.py:589: in test_sse_event_format_compliance
async for event in stream:
tck/transport/rest_client.py:329: in send_streaming_message
raise TransportError(error_msg, TransportType.REST)
E tck.transport.base_client.TransportError: [REST] Unexpected error in REST streaming: Event loop is closed
------------------------------------------------------------------------------------------------- Captured log call --------------------------------------------------------------------------------------------------
ERROR tck.transport.rest_client:rest_client.py:328 Unexpected error in REST streaming: Event loop is closed
============================================================================================== short test summary info ===============================================================================================
FAILED tests/optional/capabilities/test_streaming_methods.py::test_sse_event_format_compliance - tck.transport.base_client.TransportError: [REST] Unexpected error in REST streaming: Event loop is closed
================================================================================ 1 failed, 20 passed, 52 skipped, 1 xfailed in 51.61s ================================================================================
⬅️ [rest] Exit code: 1
```

### Code of Conduct

- [x] I agree to follow this project's Code of Conduct

Guía de contribución

Abrir la guía de contribución

Evaluación

Este issue todavía no se ha evaluado.

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.