aio-libs / aio-libs/aiohttp

Client timeout includes connection pool queue wait time

Đang mở
#10,313 6 bình luận 1 reaction 0 người được giao Xem trên GitHub
enhancement
Ngôn ngữ chính
Python
Star
16.5k
Fork
2.4k
Merge trung bình
17 giờ 22 phút
Pull request đã merge (30 ngày)
212

Mô tả

### Describe the bug

The issue lies in how aiohttp handles timeouts when using ClientSession and connection pooling. Currently, the timeout starts counting from the moment the coroutine session.get() is called, not from when the connection is acquired from the connection pool. This means that the time spent waiting in the connection pool queue is also included in the timeout calculation, leading to unexpected TimeoutError exceptions even if the actual request could be completed within the specified timeout.

This behavior can cause issues when the connection pool is overloaded with many simultaneous requests, as clients experience timeouts primarily due to delays in acquiring a connection rather than delays in the actual request processing.

### To Reproduce

- Use the following server.py and client.py to replicate the issue.
- Run the server first and then execute the client.
- Observe the output and note the high count of `TimeoutError`

**server.py**

```python
import asyncio

from aiohttp import web

async def main(_):
await asyncio.sleep(1)
return web.Response()

app = web.Application()
app.add_routes([web.get('/', main)])
web.run_app(app)
```

**client.py**

```python
import asyncio
import time
import statistics as stat

import aiohttp

async def request(session) -> tuple[bool | None, float]:
timeout_error = None
start_time = time.time()

try:
async with session.get('/'):
pass

timeout_error = False
except TimeoutError:
timeout_error = True
finally:
end_time = time.time()
duration = end_time - start_time

return timeout_error, duration

async def main(n: int=1000):
timeout = aiohttp.ClientTimeout(total=5)

async with aiohttp.ClientSession('http://127.0.0.1:8080', timeout=timeout) as session:
async with asyncio.TaskGroup() as tg:
tasks = [tg.create_task(request(session)) for _ in range(n)]

return [task.result() for task in tasks]

if __name__ == '__main__':
results: list[tuple[bool | None, float]] = asyncio.run(main())
assert len(results) == len([x for x in results if x[0] is not None])
durations = [x[1] for x in results]
print(f'Requests count: {len(results)}')
print(f'Timeout Error count: {len([x for x in results if x[0] is True])}')
print(f'First request time: {durations[0]}')
print(f'Last request time: {durations[-1]}')
print(f'Median request time: {stat.median(durations)}')
```

Run a server as follows:

```
python server.py
```

Then run a client:

```
python client.py
```

Despite setting the timeout to 5 seconds, many requests fail with TimeoutError.
The observed behavior indicates that the timeout starts before acquiring a connection from the pool, rather than only including the time for the actual request.

### Expected behavior

All requests will be executed successfully without a TimeoutError.
The timeout should start counting only after a connection is acquired from the connection pool. This ensures that the specified timeout duration applies exclusively to the actual request-processing time, making the timeout behavior predictable and intuitive.

### Logs/tracebacks

```python-traceback
$ python client.py

Requests count: 1000
Timeout Error count: 500
First request time: 1.070129156112671
Last request time: 5.747848749160767
Median request time: 5.41119110584259
```

### Python Version

```console
3.13.0
```

### aiohttp Version

```console
3.11.11
```

### multidict Version

```console
6.1.0
```

### propcache Version

```console
0.2.1
```

### yarl Version

```console
1.18.3
```

### OS

Debian GNU/Linux 11 (bullseye)

### Related component

Client

### Additional context

_No response_

### Code of Conduct

- [X] I agree to follow the aio-libs Code of Conduct

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Đánh giá

Issue này chưa được đánh giá.

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.