본문으로 건너뛰기

[Open WebUI] 메모리 리셋 API에서 커넥션 풀 고갈을 방지하는 치명적 버그 수정

PR 링크: open-webui/open-webui#20580 상태: Merged | 변경: +9 / -2

들어가며

Open WebUI의 POST /reset 엔드포인트에서 Depends(get_session)으로 DB 세션을 주입받은 후, 사용자의 모든 메모리에 대해 asyncio.gather()로 병렬 임베딩 API 호출을 수행하고 있었습니다. 100개의 메모리를 가진 사용자의 경우, 하나의 DB 커넥션이 100개의 임베딩 호출(각각 1-5초)이 완료될 때까지 수 분간 점유됩니다. 한 명의 사용자가 이 엔드포인트를 호출하면 커넥션 풀이 완전히 고갈되어 QueuePool timeout 에러가 전체 애플리케이션에서 발생할 수 있습니다.

핵심 코드 분석

Before: 세션이 임베딩 호출 전체를 점유

@router.post("/reset")
async def reset_memory_from_vector_db(
    request: Request,
    user=Depends(get_verified_user),
    db: Session = Depends(get_session),  # 커넥션을 점유한 채로
):
    memories = Memories.get_memories_by_user_id(user.id, db=db)

    # 100개의 병렬 임베딩 호출 동안 db 세션(커넥션)이 계속 열려있음
    vectors = await asyncio.gather(
        *[generate_embedding(m.content) for m in memories]
    )

After: 세션 의존성 제거, 자체 관리 세션 사용

@router.post("/reset")
async def reset_memory_from_vector_db(
    request: Request,
    user=Depends(get_verified_user),
    # Depends(get_session) 제거!
):
    """CRITICAL: 의도적으로 Depends(get_session)을 사용하지 않음.
    이 엔드포인트는 asyncio.gather()로 모든 메모리의 임베딩을
    동시에 생성합니다. 세션을 들고 있으면 커넥션 풀이 고갈됩니다."""

    memories = Memories.get_memories_by_user_id(user.id)
    # get_memories_by_user_id가 자체적으로 짧은 세션을 열고 즉시 반환

    vectors = await asyncio.gather(
        *[generate_embedding(m.content) for m in memories]
    )

왜 이게 좋은가

  • 치명적 버그 수정: 단 한 명의 사용자가 /reset을 호출하면 전체 애플리케이션의 DB 접근이 마비될 수 있었습니다.
  • 최소 변경으로 최대 효과: db: Session = Depends(get_session) 파라미터를 제거하고 db=db를 빼는 것만으로 해결됩니다.
  • 방어적 문서화: 왜 세션 의존성을 사용하지 않는지 docstring에 명확히 기록하여 향후 누군가가 다시 추가하는 것을 방지합니다.
  • 설계 교훈: DB 커넥션의 수명은 실제 DB 작업과 일치시켜야 합니다. 외부 API 호출 동안 커넥션을 보유하면 안 됩니다.

참고 자료

댓글

관련 포스트

PR Analysis 의 다른글