From 12aa55ed1469e5af7d3a897bad072ef4e49119c8 Mon Sep 17 00:00:00 2001 From: gahusb Date: Fri, 3 Jul 2026 01:47:34 +0900 Subject: [PATCH] =?UTF-8?q?feat(trade-monitor):=20FastAPI=20lifespan=20+?= =?UTF-8?q?=20heartbeat=20=EB=B0=B0=EC=84=A0=20+=20/health?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- services/trade-monitor/main.py | 62 +++++++++++++++++++++ services/trade-monitor/tests/test_health.py | 6 ++ 2 files changed, 68 insertions(+) create mode 100644 services/trade-monitor/main.py create mode 100644 services/trade-monitor/tests/test_health.py diff --git a/services/trade-monitor/main.py b/services/trade-monitor/main.py new file mode 100644 index 0000000..d0fa302 --- /dev/null +++ b/services/trade-monitor/main.py @@ -0,0 +1,62 @@ +"""trade-monitor FastAPI entry — lifespan(monitor_loop + heartbeat_loop) + /health.""" +from __future__ import annotations + +import asyncio +import logging +from contextlib import asynccontextmanager + +import redis.asyncio as aioredis +from fastapi import FastAPI + +import monitor +from config import load_settings +from kis_client import KISClient +from nas_client import NASClient +from _shared.heartbeat import heartbeat_loop, WorkerStats + +logging.basicConfig(level=logging.INFO, + format="%(asctime)s %(name)s %(levelname)s %(message)s") +logger = logging.getLogger(__name__) + +HEARTBEAT_INTERVAL = 15 # 60초 루프 > TTL 45초 → 독립 15초 발신으로 만료갭 해소 +HEARTBEAT_TTL = 45 + + +@asynccontextmanager +async def lifespan(app: FastAPI): + settings = load_settings() + nas = NASClient(settings.nas_base_url, settings.webai_api_key) + kis = KISClient(settings.kis_app_key, settings.kis_app_secret, + settings.kis_account, settings.kis_is_virtual) + state = monitor.MonitorState() + stats = WorkerStats() + redis = aioredis.from_url(settings.redis_url, decode_responses=False) + + mon_task = asyncio.create_task( + monitor.monitor_loop(nas, kis, state, stats, settings)) + hb_task = asyncio.create_task(heartbeat_loop( + redis, "trade-monitor", "trader", stats, + interval=HEARTBEAT_INTERVAL, ttl=HEARTBEAT_TTL, + state_fn=monitor.make_state_fn(state))) + logger.info("trade-monitor lifespan 시작") + try: + yield + finally: + for t in (mon_task, hb_task): + t.cancel() + try: + await t + except asyncio.CancelledError: + pass + await kis.close() + await nas.close() + await redis.aclose() + logger.info("trade-monitor lifespan 종료") + + +app = FastAPI(lifespan=lifespan) + + +@app.get("/health") +def health(): + return {"ok": True, "service": "trade-monitor"} diff --git a/services/trade-monitor/tests/test_health.py b/services/trade-monitor/tests/test_health.py new file mode 100644 index 0000000..0ec996a --- /dev/null +++ b/services/trade-monitor/tests/test_health.py @@ -0,0 +1,6 @@ +"""/health — 라우트 핸들러 직접 검증.""" +from main import health + + +def test_health(): + assert health() == {"ok": True, "service": "trade-monitor"}