From a1548c77d7e8ab638b43b7cb474be6353d6f6e2c Mon Sep 17 00:00:00 2001 From: Quant Trader Date: Mon, 6 Jul 2026 14:16:49 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=9C=20?= =?UTF-8?q?=EC=B2=AB=20=EB=A1=9C=EB=93=9C=20=EB=B8=94=EB=A1=9C=ED=82=B9=20?= =?UTF-8?q?=ED=95=B4=EC=86=8C=20=E2=80=94=20=EB=8F=99=EA=B8=B0=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=EB=A5=BC=20=EC=8A=A4=EB=A0=88=EB=93=9C=EB=A1=9C=20?= =?UTF-8?q?=EA=B2=A9=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 첫 접속 시 운영 상태(시장 국면 실시간 조회, 동기 네트워크 수십 초 가능)가 aiohttp 이벤트 루프를 잡아 첫 페이지·내 자산·차트까지 전부 함께 멈췄다 (실브라우저 검증에서 스크린샷 타임아웃으로 실측된 문제). 수정: 동기 작업이 섞이는 세 핸들러를 asyncio.to_thread로 격리 - /api/runtime: 시장 국면 실시간 조회 - /api/basket_evaluation: 캐시 미스 시 수집기(TradingHours·잠재 pykrx) - /api/portfolio: live 모드 KIS 잔고 조회 대비 실측(수정 후): /api/runtime 진행 중에도 /api/baskets 53ms, 첫 페이지 23ms — 내 돈 화면은 항상 즉시 뜨고 운영 상태만 나중에 채워진다. 전체 스위트 1638 통과. --- monitoring/web_dashboard.py | 37 ++++++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/monitoring/web_dashboard.py b/monitoring/web_dashboard.py index dd3e0e0b..d1f3097c 100644 --- a/monitoring/web_dashboard.py +++ b/monitoring/web_dashboard.py @@ -754,8 +754,11 @@ async def handle_index(_request: web.Request) -> web.Response: async def handle_api_portfolio(_request: web.Request) -> web.Response: + # live 모드에서는 KIS 잔고 조회(동기 네트워크)가 섞일 수 있다 — 스레드로 격리. + import asyncio + try: - data = get_portfolio_json() + data = await asyncio.to_thread(get_portfolio_json) return web.json_response(data) except Exception as e: logger.exception("API /api/portfolio 오류: {}", e) @@ -819,8 +822,14 @@ async def handle_api_cash_flows(request: web.Request) -> web.Response: async def handle_api_runtime(_request: web.Request) -> web.Response: + # get_runtime_json은 시장 국면 실시간 조회(동기 네트워크, 수십 초 가능)를 포함한다 — + # 이벤트 루프에서 직접 부르면 그동안 '/'·내 자산·차트까지 전부 멈춘다(첫 로드 체감 저하). + # 스레드로 내려 다른 엔드포인트는 즉시 응답하게 한다. + import asyncio + try: - return web.json_response(get_runtime_json()) + data = await asyncio.to_thread(get_runtime_json) + return web.json_response(data) except Exception as e: logger.exception("API /api/runtime 오류: {}", e) return web.json_response({"error": str(e)}, status=500) @@ -851,16 +860,10 @@ async def handle_api_basket_evaluation(_request: web.Request) -> web.Response: read-only. include_benchmark=False로 네트워크(KS11 조회)를 피한다 — 대시보드는 10초 폴링이므로 외부 조회를 섞으면 안 된다. 결과는 60초 TTL 캐시. """ + import asyncio import time as _time - try: - now = _time.monotonic() - if ( - _BASKET_EVAL_CACHE["data"] is not None - and now - _BASKET_EVAL_CACHE["at"] < _BASKET_EVAL_TTL_SEC - ): - return web.json_response(_BASKET_EVAL_CACHE["data"]) - + def _collect_all() -> dict: from core.basket_evaluation import collect_basket_paper_evaluation from core.basket_rebalancer import BasketRebalancer @@ -877,7 +880,19 @@ async def handle_api_basket_evaluation(_request: web.Request) -> web.Response: "snapshot_coverage": result.get("snapshot_coverage"), "issues": result.get("issues", []), }) - payload = {"evaluations": out} + return {"evaluations": out} + + try: + now = _time.monotonic() + if ( + _BASKET_EVAL_CACHE["data"] is not None + and now - _BASKET_EVAL_CACHE["at"] < _BASKET_EVAL_TTL_SEC + ): + return web.json_response(_BASKET_EVAL_CACHE["data"]) + + # 수집기는 TradingHours 초기화·(잠재) pykrx 갱신 등 동기 작업 — 스레드로 내려 + # 캐시 미스 시에도 이벤트 루프가 다른 요청을 계속 처리하게 한다. + payload = await asyncio.to_thread(_collect_all) _BASKET_EVAL_CACHE["at"] = now _BASKET_EVAL_CACHE["data"] = payload return web.json_response(payload)