Timeout expired prior to obtaining a connection from the pool
Dapper.QueryAsync
그리고 추가로:
An item with the same key has already been added
의 오류들이 나고 있다는 것을 트러블 슈팅을 하며 알아 냈다.
2. 처음 했던 오해
MySQL 서버가 스케일업 중 30분간 다운
요청이 쌓였다가 복구 시 커넥션 경쟁 발생
스레드 부족으로 서버 다운
하지만 개발 환경에서 확인해보니:
단 1건 요청으로도 서버가 즉시 종료됨
트래픽 문제가 아니라 구조 문제였다.
3. 핵심 원인
예외가 발생한 것이 아니라, 예외가 Task로 전달되지 않은 것
Task vs Timer
비동기로 호출 되었지만 문제는 그것이 아니다.
“예외가 Task 안에 있느냐 / 밖에 있느냐”
정상 구조 (MSSQL)
비즈니스 서버는 기본적으로 mssql과 dapper로 ORM을 구성하여 사용했는데
이에 사용한 드라이버가 System.Data.SqlClient이다.
주로 .NET에서 사용하는 전용 드라이버다.
System.Data.SqlClient (예시)
var tcs = new TaskCompletionSource<T>();
BeginIo(
result => tcs.SetResult(result),
error => tcs.SetException(error)
);
return tcs.Task;
흐름
백그라운드 스레드
→ 예외 발생
→ Task.SetException
→ await에서 throw
→ try-catch 처리
예외가 발생하여도 항상 Task를 통해 예외 전달됨(중요)
문제 구조 (MySQL)
문제는 비즈니스 서버는 mysql을 딱 한곳만 사용하는데 MySql.Data.MySqlClient 로 ORM을 연결하여 구현되어 있었다.
문제는 같은 ORM 래퍼라도 내부 동작이 달랐다.
MySql.Data(예시)
var cts = new CancellationTokenSource(timeout);
var connectTask = tcp.ConnectAsync(...);
var winner = await Task.WhenAny(
connectTask,
Task.Delay(timeout, cts.Token)
);
if (winner != connectTask)
throw new MySqlException("Timeout");