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 에러가 사라진 것으로 확인했다.