Skip to content

perf(ws_agent): optimize subscription speed, isolate sync handlers, fix task leaks#41

Merged
unohee merged 1 commit into
mainfrom
perf/ws-agent-optimizations
Jun 4, 2026
Merged

perf(ws_agent): optimize subscription speed, isolate sync handlers, fix task leaks#41
unohee merged 1 commit into
mainfrom
perf/ws-agent-optimizations

Conversation

@unohee

@unohee unohee commented Jun 4, 2026

Copy link
Copy Markdown
Owner

Summary

WSAgent의 4가지 최적화·안정성 개선:

  1. 구독 속도 N배 개선 (불필요한 sub 간 0.5s sleep 제거)
  2. Sync 핸들러를 `asyncio.to_thread()`로 격리 — 수신 루프 블록 방지
  3. Fire-and-forget 백그라운드 task 참조 보관 + disconnect 시 cancel
  4. 구독 응답 state 누수 fix + JSON 이중 파싱 제거

Why

1. 구독 속도

`_subscribe_all`이 각 구독 후 `await asyncio.sleep(0.5)` — KIS rate limit 우려로 추가됐지만 `_send_subscription` 자체가 응답 이벤트를 기다려 자연스럽게 직렬화됨. 50종목 구독 시 추가로 ~25초 소비. 응답 후 즉시 다음 요청을 보내도 KIS rate limit에 문제 없음.

2. Sync 핸들러 블로킹

`_call_handler`가 sync 핸들러를 직접 호출 → CPU/IO 무거운 핸들러가 `_receive_loop`를 통째로 블록 → ping/pong 응답 지연 → 강제 재연결. `asyncio.to_thread()`로 격리하면 핸들러가 무거워도 수신 루프는 계속 동작.

3. Task GC 위험

`subscribe()`의 `asyncio.create_task(self._send_subscription(...))`와 `unsubscribe()`의 fire-and-forget이 반환 task를 보관 안 함. Python 3.10+에서 weakly-referenced task가 GC로 사라질 수 있다는 문서화된 동작. disconnect 시 정리도 안 됨.

4. State 누수 + 이중 파싱

  • `_send_subscription` finally가 `_pending_subscriptions`만 정리 → `_subscription_results`/`_errors` 남음 → 재시도 시 stale data
  • `_handle_message`에서 JSON 파싱 후 `_parse_message`가 다시 `json.loads` 호출

Changes

영역 변경
`ws_agent.py:_subscribe_all` sub 간 `asyncio.sleep(0.5)` 제거, `list(.values())` 한 번만 호출하도록 정리
`ws_agent.py:_call_handler` sync 핸들러 → `asyncio.to_thread()`, 핸들러 예외 잡아 stats 증가만
`ws_agent.py:init` `_background_tasks: Set[asyncio.Task]` 추가
`ws_agent.py` `_track_task()` 헬퍼 추가, `subscribe`/`unsubscribe`의 create_task가 이를 통하도록
`ws_agent.py:disconnect` 미완료 백그라운드 task cancel + gather
`ws_agent.py:_send_subscription` finally에서 results/errors도 pop
`ws_agent.py:_parse_message` `json_data` optional 파라미터 추가 (이미 파싱된 dict 재사용)
`ws_agent.py:_handle_message` JSON 한 번 파싱 후 `_parse_message`에 넘김
`tests/unit/test_ws_agent_optimizations.py` (NEW) 9개 검증 테스트

Test plan

  • 신규 9개 단위 테스트 (subscribe_all 속도, 핸들러 격리, task tracking, state cleanup, JSON 재사용)
  • 기존 ws 단위 테스트 144/145 passed (1 pre-existing failure: `test_fatal_error_patterns_exist` — main에서도 실패, 본 변경과 무관)
  • `ruff check` — All checks passed
  • (선택) 실제 KIS 모의/실전 환경에서 50종목 동시 구독 시간 측정

Compatibility

  • 공개 API 시그니처 변경 없음 (`subscribe`/`unsubscribe`/`connect`/`disconnect`/`register_handler`/`set_default_handler` 모두 동일)
  • 동기 핸들러를 사용 중인 사용자: 핸들러가 `to_thread`에서 실행되므로 thread-safety 책임이 사용자 쪽으로 — 핸들러 안에서 공유 상태 변경 시 lock 필요

…ix task leaks

WSAgent 4가지 최적화·안정성 개선.

1. 순차 구독 속도 N배 개선
   - _subscribe_all에서 구독 사이 0.5초 sleep 제거
   - _send_subscription이 응답 대기로 자연 직렬화되므로 추가 sleep 불필요
   - 50 종목 구독 시 ~25초 단축 (이전: 0.5s × N 추가 소비)

2. Sync 핸들러 격리 — 수신 루프 블록 방지
   - _call_handler에서 sync 핸들러를 asyncio.to_thread()로 격리
   - 이전: 무거운 sync 핸들러가 _receive_loop 자체를 블록 → ping/pong 응답
     지연 → 강제 재연결 발생 가능
   - 핸들러 예외도 _call_handler가 잡아 stats["errors"] 증가만 시키고 다른
     메시지 처리는 계속 진행

3. Background task 참조 보관 + 정리
   - subscribe()/unsubscribe()의 fire-and-forget asyncio.create_task가
     반환 task를 어디에도 저장하지 않아 GC로 사라질 위험 (Python 3.10+
     문서화된 동작)
   - _background_tasks set + _track_task() 헬퍼로 강한 참조 보관, 완료 시
     done_callback으로 자동 제거
   - disconnect() 시 미완료 task 일괄 cancel + gather

4. 구독 응답 state 정리 누수 fix + JSON 이중 파싱 제거
   - _send_subscription의 finally에서 _subscription_results/_errors도
     pop (이전엔 _pending만 정리되어 재시도 시 stale 데이터 사용 위험)
   - _handle_message에서 JSON 파싱한 결과를 _parse_message에 넘겨
     이중 json.loads() 호출 제거

검증:
- 신규 단위 테스트 9개 (tests/unit/test_ws_agent_optimizations.py):
  * _subscribe_all 5개 구독이 0.3초 미만에 완료
  * sync 핸들러가 메인 스레드와 다른 스레드에서 실행됨 (격리)
  * async 핸들러는 이벤트 루프에서 직접 await
  * 핸들러 예외가 _call_handler에서 잡혀 전파되지 않음
  * _track_task가 done_callback으로 자동 제거됨
  * disconnect가 미완료 task 모두 cancel
  * _send_subscription 실패 후 pending/results/errors 모두 정리
  * _parse_message가 preparsed json_data 재사용 (이중 파싱 회피)
- 기존 ws 단위 테스트 144/145 passed (1 pre-existing failure: test_fatal_
  error_patterns_exist — 본 변경과 무관)
- ruff: All checks passed
@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown

📊 Coverage Analysis Report

Metric Value
Total Coverage 0%
Target Coverage 70%
Files Below Target 0

✅ All files meet the target coverage of 70%!

@unohee unohee merged commit e762d1e into main Jun 4, 2026
7 checks passed
@unohee unohee deleted the perf/ws-agent-optimizations branch June 4, 2026 04:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant