본문으로 건너뛰기

[Open WebUI] 이벤트 이미터에서 DB 호출 비동기화, 중간 상태 저장 제거, elif 체인 적용

PR 링크: open-webui/open-webui#22107 상태: Merged | 변경: +37 / -21

들어가며

Open WebUI의 소켓 이벤트 이미터는 실시간 채팅 저장이 활성화된 상태에서 스트리밍 중 핫 패스(hot path)입니다. 이 함수에 세 가지 성능 문제가 있었습니다: (1) 동기 DB 호출이 이벤트 루프를 차단하여 N명의 동시 사용자가 모든 쓰기를 직렬화함, (2) 중간 상태(도구 호출 진행, 웹 검색 진행 등)를 매번 DB에 저장하지만 이는 소켓으로 이미 전달되는 일시적 데이터임, (3) 이벤트 타입이 상호 배타적임에도 if/if/if 체인으로 매번 모든 조건을 검사함.

핵심 코드 분석

Before: 동기 DB 호출 + 모든 상태 저장 + if 체인

if "type" in event_data and event_data["type"] == "status":
    Chats.add_message_status_to_chat_by_id_and_message_id(...)

if "type" in event_data and event_data["type"] == "message":
    message = Chats.get_message_by_id_and_message_id(...)
    # ...
    Chats.upsert_message_to_chat_by_id_and_message_id(...)

if "type" in event_data and event_data["type"] == "replace":
    Chats.upsert_message_to_chat_by_id_and_message_id(...)

After: 비동기 DB + 최종 상태만 저장 + elif 체인

event_type = event_data.get("type")

if event_type == "status":
    status_data = event_data.get("data", {})
    if status_data.get("done", False):
        await asyncio.to_thread(
            Chats.add_message_status_to_chat_by_id_and_message_id, ...)

elif event_type == "message":
    message = await asyncio.to_thread(
        Chats.get_message_by_id_and_message_id, ...)
    # ...
    await asyncio.to_thread(
        Chats.upsert_message_to_chat_by_id_and_message_id, ...)

elif event_type == "replace":
    await asyncio.to_thread(
        Chats.upsert_message_to_chat_by_id_and_message_id, ...)

왜 이게 좋은가

  • 이벤트 루프 차단 해제: asyncio.to_thread()로 모든 동기 DB 호출을 스레드 풀로 위임하여, 다른 사용자의 소켓 이벤트 전달이 블로킹되지 않습니다.
  • 불필요한 I/O 제거: 중간 상태 이벤트(도구 호출 진행률 등)는 소켓으로 이미 실시간 전달되므로, done=True인 최종 상태만 DB에 저장합니다.
  • 불필요한 비교 제거: elif 체인으로 첫 매칭 후 나머지 조건을 건너뜁니다. 이벤트 타입은 상호 배타적이므로 의미적으로도 정확합니다.

참고 자료

댓글

관련 포스트

PR Analysis 의 다른글