Python asyncio 타임아웃 처리 중 발견한 함정
문제 상황
외부 API 호출이 간헐적으로 멈추는 이슈로 인해 asyncio.wait_for()로 타임아웃을 적용했다. 그런데 타임아웃 발생 후 연결이 제대로 닫히지 않아 connection pool이 고갈되는 현상이 발생했다.
async def fetch_data(session, url):
async with session.get(url) as response:
return await response.json()
# 문제가 있던 코드
try:
result = await asyncio.wait_for(fetch_data(session, url), timeout=5.0)
except asyncio.TimeoutError:
logger.error("Timeout occurred")
원인
asyncio.wait_for()는 타임아웃 시 대상 태스크에 CancelledError를 발생시킨다. 하지만 async with 블록 내부에서 cancel이 발생하면 __aexit__가 호출되기 전에 중단될 수 있다.
aiohttp 세션의 경우 응답 본문을 읽는 중에 cancel이 발생하면 연결이 재사용 불가 상태로 남았다.
해결
타임아웃을 aiohttp 자체 기능으로 이동시켰다.
import aiohttp
from aiohttp import ClientTimeout
timeout = ClientTimeout(total=5.0)
async with aiohttp.ClientSession(timeout=timeout) as session:
try:
async with session.get(url) as response:
return await response.json()
except asyncio.TimeoutError:
logger.error("Timeout occurred")
aiohttp는 내부적으로 타임아웃을 처리하므로 연결 정리가 보장된다.
교훈
- asyncio.wait_for()는 범용 타임아웃 도구지만, 라이브러리 자체 타임아웃 기능이 있다면 그쪽이 안전하다
- CancelledError가 발생하는 지점에 따라 리소스 정리 동작이 달라질 수 있다
- 비동기 컨텍스트 매니저 내부에서의 취소는 주의가 필요하다
프로덕션에서 connection pool exhausted 에러가 사라진 것으로 확인했다.