fix: DB pool_pre_ping 제거하고 pool_recycle 1800 으로 조정
ai-home-poc 과 동일한 문제. 두 엔진(engine, knowledge_engine) 모두 해당됐다.
SQLAlchemy 의 asyncpg 다이얼렉트는 ping 을 autocommit 이 아니라 명시적 트랜잭션으로 감싼다 — BEGIN -> 빈 쿼리 -> ROLLBACK (dialects/postgresql/asyncpg.py _async_ping). PgBouncer transaction/statement 모드 대응(sqlalchemy#10226) 워크어라운드인데 우리는 5432 로 직결한다. 그 결과 모든 체크아웃마다 ROLLBACK 1건이 발생해 pg_stat_database 의 rollback 비율이 ~50% 에 고정되고 왕복 3회를 더 냈다.
connect_args 의 statement_cache_size=0 은 초기 스캐폴딩에서 온 값이라 이 문제와 무관하다 — 캐시를 꺼도 위 트랜잭션 래핑은 조건 없이 실행된다(실측으로 확인).
pool_recycle 은 300 -> 1800. pre-ping 을 끈 뒤로는 이 값이 끊긴 커넥션에 대한 유일한 방어선이라 경로상 idle timeout(T) 보다 짧아야 한다. 1800 의 근거는 같은 dop-ai 네임스페이스 자바 서비스들이 HikariCP maxLifetime=1800s 로 도는데 커넥션 재생성률이 커넥션당 시간당 2.0 회(=3600/1800, maxLifetime 만으로 교체될 때의 기대값)라는 실측이다. 6h 기준 1.99~2.18, 전 플랫폼 p99 = 2.35 → T > 1800.
300 은 최초 스캐폴딩 커밋에서 온 값이고 튜닝된 값이 아니었다. 특히 knowledge 풀은 운영에서 dispatch_mode=queue 라 폴러 없이 이벤트 구동이고(폴러는 local 전용) 체크아웃이 띄엄띄엄해서, 짧은 recycle 이면 잡 간격만 넘겨도 매번 재접속을 물었다.
config_local.yml 의 database_supabase 만 300 을 유지한다 — 포트 6543 은 Supabase transaction pooler 라 인터넷 경유고 위 T 근거가 적용되지 않는 경로다(주석 명시).
검증:
- 이 파일의 실제 설정으로 통제 실험: rollback 비율 49.2% -> 3.1%
- 두 엔진 모두 pre_ping=False / recycle=1800 을 실제 import 경로로 확인 (DB_PROVIDER=local 1800, supabase 300 분기까지)
- 전체 테스트 664 passed
Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com