Skip to content

[Feat] SSE 이벤트 인프라 구현#11

Open
haein45 wants to merge 3 commits into
devfrom
feat/9-sse-infra
Open

[Feat] SSE 이벤트 인프라 구현#11
haein45 wants to merge 3 commits into
devfrom
feat/9-sse-infra

Conversation

@haein45
Copy link
Copy Markdown
Collaborator

@haein45 haein45 commented May 14, 2026

📌 관련 이슈

🏷️ PR 타입

  • ✨ 기능 추가 (Feature)
  • 🐛 버그 수정 (Bug Fix)
  • ♻️ 리팩토링 (Refactoring)
  • 📝 문서 수정 (Documentation)
  • 🎨 스타일 변경 (Style)
  • ✅ 테스트 추가 (Test)

📝 작업 내용

  • src/proovy_agent/common/sse/events.py 구현 — 이벤트 타입 정의 (SSEEvent, EventType)
  • src/proovy_agent/common/sse/emitter.py 구현 — async 큐 기반 이벤트 발행 클래스 (SSEEmitter)
  • stream() 제너레이터로 EventSourceResponse에 바로 연결 가능
  • 지원 이벤트: page_start, solve_progress, token, tool_start, tool_result, image_placeholder, image_result, node_result, credit_settled, error

📸 스크린샷

  • 해당 없음 (인프라 코드)

✅ 체크리스트

  • 코드 리뷰를 받을 준비가 완료되었습니다
  • 테스트를 작성하고 모두 통과했습니다
  • 문서를 업데이트했습니다 (필요한 경우)
  • 코드 스타일 가이드를 준수했습니다
  • 셀프 리뷰를 완료했습니다

📎 기타 참고사항

  • 인프라 코드라 별도 테스트 미작성

Summary by CodeRabbit

  • 새로운 기능
    • 서버에서 클라이언트로 실시간 이벤트를 스트리밍하는 기능이 추가되었습니다.
    • 비동기 큐 기반의 안정적인 이벤트 전송으로 병렬 처리 상황에서도 일관된 스트리밍을 보장합니다.
    • 이벤트 페이로드를 JSON으로 직렬화하여 다양한 클라이언트에서 바로 처리할 수 있습니다.

Review Change Stack

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 14, 2026

Warning

Rate limit exceeded

@haein45 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 15 minutes and 13 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4a9304b0-2279-4ae1-8868-c864d291a81b

📥 Commits

Reviewing files that changed from the base of the PR and between 8fee999 and b81ca1f.

📒 Files selected for processing (3)
  • src/proovy_agent/common/sse/emitter.py
  • src/proovy_agent/common/sse/events.py
  • tests/common/sse/test_emitter.py
📝 Walkthrough

Walkthrough

SSE 이벤트 타입과 Pydantic 모델을 추가하고, asyncio.Queue 기반의 SSEEmitter를 구현해 이벤트 발행, 종료 신호, 그리고 비동기 제너레이터 형태의 스트리밍을 제공합니다.

Changes

SSE 이벤트 인프라

Layer / File(s) Summary
이벤트 타입 및 모델 정의
src/proovy_agent/common/sse/events.py
EventType 리터럴 타입으로 지원하는 SSE 이벤트명을 열거하고, SSEEvent Pydantic 모델이 {event, data} 구조를 sse-starlette 호환 딕셔너리로 직렬화하는 to_sse() 메서드 제공.
SSE 이미터 구현
src/proovy_agent/common/sse/emitter.py
asyncio.Queue 기반 SSEEmitter 클래스가 이벤트 발행 (emit), 스트림 종료 신호 (close), 그리고 이벤트 페이로드를 비동기로 산출하는 제너레이터 (stream) 제공.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant SSEEmitter
  participant Queue
  participant Consumer

  Client->>SSEEmitter: emit(event, data)
  SSEEmitter->>Queue: put(SSEEvent)
  Client->>SSEEmitter: close()
  SSEEmitter->>Queue: put(None)
  Consumer->>SSEEmitter: stream()
  loop until None
    SSEEmitter->>Queue: get()
    SSEEmitter->>Consumer: yield to_sse()
  end
Loading

전체 요약

SSE 기반 실시간 이벤트 스트리밍 인프라의 기초 계층을 구현합니다. 이벤트 타입과 데이터 모델, 비동기 큐 기반 이미터를 제공하여 LangGraph 실행 중 발생하는 이벤트를 클라이언트에 실시간으로 전달할 수 있도록 합니다.

코드 리뷰 예상 시간

🎯 3 (Moderate) | ⏱️ ~20분

🐰 큐에 담긴 이벤트들,
비동기로 흘러가고,
클라이언트를 향해 날아가
SSE의 마법으로 ✨
실시간 스트리밍 완성!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description check ✅ Passed PR 설명이 저장소 템플릿 구조를 따르며, 관련 이슈(#9), PR 타입(기능 추가), 구체적 작업 내용, 체크리스트를 완전히 작성했습니다.
Linked Issues check ✅ Passed 이슈 #9의 모든 코딩 요구사항이 충족되었습니다: 이벤트 타입 정의(SSEEvent, EventType) [#9], async 큐 기반 에미터 클래스 [#9], 10개 이벤트 타입 지원 [#9], async 구현 및 blocking call 없음 [#9], EventSourceResponse 연결 가능 [#9].
Out of Scope Changes check ✅ Passed 모든 변경사항이 이슈 #9의 SSE 이벤트 인프라 구현 범위 내에 있으며, 범위 외 불필요한 변경사항이 발견되지 않습니다.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.
Title check ✅ Passed PR 제목이 풀 리퀘스트의 주요 변경사항을 명확히 요약하고 있습니다. SSE(Server-Sent Events) 인프라 구현이라는 핵심 목표를 간결하게 표현했습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/9-sse-infra

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/proovy_agent/common/sse/emitter.py`:
- Around line 18-28: The stream currently puts a sentinel None in close() and
then stops immediately in stream(), which allows a race where emit() can enqueue
events after close() and those events are lost; add a closed state and guard to
prevent emits after close: introduce an instance flag (e.g., self._closed) and
an asyncio.Lock (e.g., self._close_lock), set self._closed = True inside close()
while holding the lock and then put the sentinel, and modify emit(...) to
acquire the same lock or check self._closed and raise/ignore if True so no new
events are enqueued after close; keep stream() behavior of breaking on the
sentinel (None) so shutdown remains deterministic.
- Line 22: Add a return type hint to the async method stream: change its
signature to declare AsyncIterator[dict[str, str]] so it matches the other typed
methods (__init__, emit, close) and complies with the project's Python 3.12+
mandatory type-hinting guideline; ensure you import or reference
typing.AsyncIterator (or from typing import AsyncIterator) if not already
imported and update the stream method signature to use AsyncIterator[dict[str,
str]].
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: daa20db6-45c7-4e74-b14e-5a2f5a436d08

📥 Commits

Reviewing files that changed from the base of the PR and between 8eda78a and 79ee2cc.

📒 Files selected for processing (2)
  • src/proovy_agent/common/sse/emitter.py
  • src/proovy_agent/common/sse/events.py

Comment thread src/proovy_agent/common/sse/emitter.py
Comment thread src/proovy_agent/common/sse/emitter.py Outdated
@haein45 haein45 self-assigned this May 15, 2026
@haein45 haein45 added the 혜인 label May 15, 2026
- close() 호출 후 emit()이 이벤트를 enqueue하는 레이스 컨디션 수정
  (_closed 플래그 + asyncio.Lock으로 close 이후 emit 차단)
- close() 중복 호출 시 sentinel 이중 삽입 방지
- stream() 반환 타입 힌트 AsyncIterator[dict[str, str]] 추가

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@haein45 haein45 requested review from chowon442 and gaeunee2 May 15, 2026 10:14
@haein45 haein45 changed the title feat: SSE 이벤트 인프라 구현 [Feat] SSE 이벤트 인프라 구현 May 15, 2026
Copy link
Copy Markdown
Member

@chowon442 chowon442 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

인프라여도 테스트 코드 작성해주시면 좋을 것 같아요! test_emit_and_stream(), test_close_idempotent(), test_emit_after_close_ignored() 같은 것들 정도는 SSEEmitter가 순수 async 로직이라 외부 의존성이 없어서 쉽게 추가할 수 있을 것 같습니다.

마지막으로 주석들은 한국어로 작성해주시면 좋을 것 같아요~

Comment thread src/proovy_agent/common/sse/emitter.py Outdated
"""Collects SSE events and streams them via an async queue."""

def __init__(self) -> None:
self._queue: asyncio.Queue[SSEEvent | None] = asyncio.Queue()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

asyncio.Queue()에 maxsize를 설정해서 무제한 메모리 증가를 막아주면 좋을 것 같아요!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maxsize=256으로 설정했습니다!

Comment on lines +24 to +30
async def close(self) -> None:
"""Signal stream end."""
async with self._close_lock:
if self._closed:
return
self._closed = True
await self._queue.put(None)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

close() 이후 emit()이 silent drop이라서 logging.debug 있으면 디버깅하기 좋을 것 같아요!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

추가했습니다!!

Comment thread src/proovy_agent/common/sse/events.py Outdated

def to_sse(self) -> dict:
"""Return dict compatible with sse-starlette ServerSentEvent."""
import json
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to_sse() 안에서 import하는게 맞는 걸까요?

- 큐 최대 크기 제한 (maxsize=256) 으로 무제한 메모리 증가 방지
- close() 후 emit() 무시 시 logging.debug 로 디버깅 정보 기록
- events.py의 import json을 모듈 상단으로 이동
- 한국어 docstring 적용
- SSEEmitter 단위 테스트 추가
  - test_emit_and_stream: 이벤트 순서 검증
  - test_close_idempotent: close() 중복 호출 안전성 검증
  - test_emit_after_close_ignored: close 후 emit 무시 검증

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] SSE 이벤트 인프라 구현

2 participants