Skip to content

fix(rate_limiter): enforce official KIS limit, lock-free sleep, safer defaults#40

Merged
unohee merged 1 commit into
mainfrom
fix/rate-limiter-safety-margin
Jun 4, 2026
Merged

fix(rate_limiter): enforce official KIS limit, lock-free sleep, safer defaults#40
unohee merged 1 commit into
mainfrom
fix/rate-limiter-safety-margin

Conversation

@unohee

@unohee unohee commented Jun 4, 2026

Copy link
Copy Markdown
Owner

Summary

API rate limiting이 KIS 공식 한도(20 RPS / 1000 RPM)에 닿지 않도록 안전 마진 확보, 동시 호출 시 sliding window 위반 방지, lock-free sleep으로 동시 부하 차단.

Why

  1. Priority burst 버그: 이전 `acquire()`는 priority≥1 시 RPS+burst까지 허용 — 기본 18 RPS + 10 burst = 28회/초로 공식 한도 20을 초과 가능.
  2. Lock 안 sleep: `time.sleep()`을 lock 안에서 호출 → 한 스레드의 대기가 다른 스레드를 완전 블록 (동시 N개 호출 시 직렬화로 부하 폭증).
  3. 마진 불충분: 18/900 (90%)은 jitter + 누적 burst 시 위반 위험. 75~80% 마진이 안전.
  4. 공식 한도 보호 없음: 사용자가 `requests_per_second=25` 같이 잘못된 값을 줘도 그대로 통과.
  5. Sliding window corner case: `min_interval = 1/RPS` 정확히 같으면 윈도우 경계에서 N+1개 가능.
  6. DRY 위반: `client.py`와 `agent.py`에 동일 기본값이 따로 하드코딩.

Changes

영역 변경
`rate_limiter.py` `DEFAULT_RPS=15`, `DEFAULT_RPM=800`, `DEFAULT_MIN_INTERVAL_MS=70`, `DEFAULT_BURST_SIZE=3` 상수 도입
`rate_limiter.py` `KIS_OFFICIAL_RPS_LIMIT=20`, `KIS_OFFICIAL_RPM_LIMIT=1000` 절대 한도 상수 + RateLimiter 생성/set_limits 시 자동 클램프
`rate_limiter.py` `acquire()` 슬립을 lock 바깥으로 — 슬롯 예약 시간만 lock 안에서 계산, 실제 sleep은 lock 해제 후
`rate_limiter.py` priority≥1 burst도 공식 한도 절대 위반 방지: `min(RPS + burst, KIS_OFFICIAL_RPS_LIMIT)`
`rate_limiter.py` `min_interval` floor = `(1/RPS) + 0.001s` (sliding window corner case 방지) + 윈도우 끝 20ms safety padding
`client.py`, `agent.py` 하드코딩된 18/900/55ms 기본값 제거, `get_global_rate_limiter()` 호출만
`tests/unit/test_rate_limiter_safety.py` (NEW) 안전 마진/동시성/싱글턴/lock-free sleep 등 12개 테스트
`docs/RATE_LIMITER_GUIDE.md` 기본값/안전 보장 섹션 갱신

Test plan

  • `pytest tests/unit/test_rate_limiter.py test_rate_limiter_safety.py test_agent.py test_client.py test_client_comprehensive.py` — 91/91 passed
  • 동시 12개 호출 시 1초 윈도우에 ≤5개 (5 RPS 케이스)
  • 동시 30개 호출 시 1초 윈도우에 ≤15개 (기본 15 RPS)
  • priority=1 + 25개 동시 호출 시 공식 RPS 한도(20) 초과 없음
  • 한 스레드 sleep 중 `get_current_rate()` 즉시 반환 (<50ms)
  • 동시 20개 스레드의 `get_global_rate_limiter()` 호출 시 단일 인스턴스
  • `ruff check` — All checks passed

… defaults

API rate limiting이 KIS 공식 한도(20 RPS / 1000 RPM)에 닿지 않도록 안전 마진을
확보하고, 동시 호출 시에도 sliding window 위반이 발생하지 않도록 수정.

핵심 변경:

1. 기본값 강화 (75~80% 마진)
   - RPS: 18 → 15 (공식 20의 75%)
   - RPM: 900 → 800 (공식 1000의 80%)
   - min_interval_ms: 55 → 70 (15 RPS 이론적 67ms + 3ms jitter)
   - burst_size: 10 → 3 (priority>=1 시 effective RPS=18로 공식 20 안전 보장)

2. 공식 한도 절대 위반 방지
   - 사용자가 RPS>20 또는 RPM>1000 전달 시 자동 클램프
   - KIS_OFFICIAL_RPS_LIMIT(20), KIS_OFFICIAL_RPM_LIMIT(1000) 상수 도입
   - 이전 priority>=1 시 RPS + burst까지 허용해 공식 한도 초과 가능 버그 수정
   - effective RPS = min(RPS + burst, KIS_OFFICIAL_RPS_LIMIT)로 강제

3. 동시성 안전 (lock-free sleep)
   - acquire() 내부에서 time.sleep()을 lock 바깥에서 실행
   - 이전 구현은 sleep을 lock 안에서 호출해 한 스레드의 대기가 다른 스레드를
     완전히 블록 → 동시 N개 호출 시 부하 폭증
   - 슬롯 예약(reserved_until) 방식으로 동시 호출 시 각 호출에 서로 다른 미래
     슬롯을 분배

4. Sliding window 한도 정합성
   - min_interval에 (1.0/RPS + 0.001s) floor 강제
   - 슬롯 간격이 정확히 1/RPS면 윈도우 경계와 겹쳐 N+1개가 1초 안에 들어올 수
     있는 corner case 차단
   - 윈도우 끝에 20ms safety padding (OS sleep undersleep 대비)

5. 중복 설정 제거 (DRY)
   - client.py와 agent.py에 하드코딩된 동일한 18/900/55ms 기본값을 제거
   - 두 곳 모두 get_global_rate_limiter() 호출 시 모듈 상수 DEFAULT_* 사용

검증 (12개 신규 단위 테스트):
- 공식 한도 클램프 (RateLimiter, set_limits)
- 동시 12개 호출 시 1초 윈도우에 5개 이하 (5 RPS 케이스)
- 동시 30개 호출 시 1초 윈도우에 15개 이하 (기본 15 RPS)
- priority>=1 + 큰 burst여도 공식 RPS 한도(20) 초과 안 함
- 슬립이 lock 바깥에서 일어나 get_current_rate() 호출이 즉시 반환
- 전역 싱글턴이 동시 20개 스레드에서도 단일 인스턴스 보장

문서: docs/RATE_LIMITER_GUIDE.md 갱신.
@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 d344a84 into main Jun 4, 2026
7 checks passed
@unohee unohee deleted the fix/rate-limiter-safety-margin branch June 4, 2026 02:45
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