From e25f7295a67ff8e54cddee25f9dc1a18023dd942 Mon Sep 17 00:00:00 2001 From: I Luk Kim Date: Tue, 14 Apr 2026 15:39:50 -0700 Subject: [PATCH] =?UTF-8?q?fix:=20=EB=8C=80=EA=B7=9C=EB=AA=A8=20Alpaca=20?= =?UTF-8?q?=EB=B0=B1=ED=95=84=20=EC=8B=9C=20uvicorn=20=EC=9D=B4=EB=B2=A4?= =?UTF-8?q?=ED=8A=B8=20=EB=A3=A8=ED=94=84=20=ED=8F=AC=ED=99=94=20=EB=B0=A9?= =?UTF-8?q?=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 동시 Alpaca intraday 요청이 50개 이상 쌓이면 BaseHTTPMiddleware의 task 스케줄링 오버헤드로 이벤트 루프가 응답불능 상태가 되는 현상 수정. - alpaca.py: _INTRADAY_SEMAPHORE(10) 추가 — /intraday, /intraday/today 양쪽 핸들러를 감쌈. 11번째 이후 요청은 세마포어 대기(비용 없음) - docker-compose.yml: --limit-concurrency 100 추가 — 100개 초과 시 uvicorn이 503 반환, health 엔드포인트 보호 Co-Authored-By: Claude Sonnet 4.6 --- app/api/v1/endpoints/alpaca.py | 64 +++++++++++++++++++--------------- docker-compose.yml | 2 +- 2 files changed, 37 insertions(+), 29 deletions(-) diff --git a/app/api/v1/endpoints/alpaca.py b/app/api/v1/endpoints/alpaca.py index 4b56722..72a9536 100644 --- a/app/api/v1/endpoints/alpaca.py +++ b/app/api/v1/endpoints/alpaca.py @@ -2,6 +2,7 @@ Alpaca Market Data endpoints — standalone price data via Alpaca API """ +import asyncio from datetime import date, datetime, timezone, timedelta from typing import Optional from zoneinfo import ZoneInfo @@ -11,6 +12,11 @@ from fastapi import APIRouter, HTTPException, Query _ET = ZoneInfo("America/New_York") _MARKET_CLOSE_HOUR = 16 # 4:00 PM ET +# Limit concurrent Alpaca intraday processing to prevent event-loop saturation +# under bulk backfill workloads. Callers beyond this limit wait on the semaphore +# (cheap asyncio wait) rather than flooding httpx connections and DB sessions. +_INTRADAY_SEMAPHORE = asyncio.Semaphore(10) + def _market_closed_for(d: date) -> bool: """Return True if the US equity market session for date d has ended.""" @@ -120,21 +126,22 @@ async def get_alpaca_intraday_multi( start_dt = datetime.combine(_start, datetime.min.time()).replace(tzinfo=timezone.utc) end_dt = datetime.combine(_end, datetime.max.time()).replace(tzinfo=timezone.utc) - try: - data = await svc.get_or_fetch_multi_bars( - symbols, start_dt, end_dt, interval, force_refresh, feed="sip" - ) - except Exception as e: - err = str(e) - detail = f"Alpaca API error: {err}" - if "502" in err or "Bad Gateway" in err: - detail = ( - f"Alpaca 502 Bad Gateway — 요청당 심볼 수 초과 가능성. " - f"내부 배치 크기: 100개/요청. 원인: {err}" + async with _INTRADAY_SEMAPHORE: + try: + data = await svc.get_or_fetch_multi_bars( + symbols, start_dt, end_dt, interval, force_refresh, feed="sip" ) - raise HTTPException(status_code=502, detail=detail) - finally: - await svc.client.close() + except Exception as e: + err = str(e) + detail = f"Alpaca API error: {err}" + if "502" in err or "Bad Gateway" in err: + detail = ( + f"Alpaca 502 Bad Gateway — 요청당 심볼 수 초과 가능성. " + f"내부 배치 크기: 100개/요청. 원인: {err}" + ) + raise HTTPException(status_code=502, detail=detail) + finally: + await svc.client.close() bars = { ticker: [ @@ -193,21 +200,22 @@ async def get_alpaca_intraday_today( start_dt = datetime.combine(today, datetime.min.time()).replace(tzinfo=timezone.utc) end_dt = datetime.combine(today, datetime.max.time()).replace(tzinfo=timezone.utc) - try: - data = await svc.get_or_fetch_multi_bars( - symbols, start_dt, end_dt, interval, force_refresh=True, feed="iex" - ) - except Exception as e: - err = str(e) - detail = f"Alpaca API error: {err}" - if "502" in err or "Bad Gateway" in err: - detail = ( - f"Alpaca 502 Bad Gateway — 요청당 심볼 수 초과 가능성. " - f"내부 배치 크기: 100개/요청. 원인: {err}" + async with _INTRADAY_SEMAPHORE: + try: + data = await svc.get_or_fetch_multi_bars( + symbols, start_dt, end_dt, interval, force_refresh=True, feed="iex" ) - raise HTTPException(status_code=502, detail=detail) - finally: - await svc.client.close() + except Exception as e: + err = str(e) + detail = f"Alpaca API error: {err}" + if "502" in err or "Bad Gateway" in err: + detail = ( + f"Alpaca 502 Bad Gateway — 요청당 심볼 수 초과 가능성. " + f"내부 배치 크기: 100개/요청. 원인: {err}" + ) + raise HTTPException(status_code=502, detail=detail) + finally: + await svc.client.close() bars = { ticker: [ diff --git a/docker-compose.yml b/docker-compose.yml index 94d0075..144b6b8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -66,7 +66,7 @@ services: - ./yfinance_plus:/app/yfinance_plus # Mount yfinance_plus for development - ./data:/app/data # For data files restart: unless-stopped - command: ["python", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "18000", "--reload"] + command: ["python", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "18000", "--reload", "--limit-concurrency", "100"] # Frontend Application frontend: