fix(bootstrap): backend lifespan 에서 DB migrate + symbols 시드 자동화

- main.py 의 lifespan 시작 시 idempotent migration 적용 + symbols 비어있으면 pykrx 로 전 종목 시드
- BOOTSTRAP_DISABLED=1 / SCHEDULER_DISABLED=1 env 로 비활성 가능 (테스트 용)
- 실패해도 서버는 뜨고 /health/db 가 진단 제공

리뷰어 지적 1번 (cold-start 시 /api/refresh 404) 해결.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
tkrmagid
2026-05-20 16:26:59 +09:00
parent bc016ab76d
commit f84b460e54

View File

@@ -1,10 +1,12 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
import os
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy import text
from app.api.chart import router as chart_router from app.api.chart import router as chart_router
from app.api.metrics import router as metrics_router from app.api.metrics import router as metrics_router
@@ -13,7 +15,7 @@ from app.api.predict import router as predict_router
from app.api.refresh import router as refresh_router from app.api.refresh import router as refresh_router
from app.api.symbols import router as symbols_router from app.api.symbols import router as symbols_router
from app.config import settings from app.config import settings
from app.db.connection import ping as db_ping from app.db.connection import get_engine, ping as db_ping
from app.fetch import dart as dart_mod from app.fetch import dart as dart_mod
from app.fetch import kis as kis_mod from app.fetch import kis as kis_mod
from app.pipelines.scheduler import shutdown_scheduler, start_scheduler from app.pipelines.scheduler import shutdown_scheduler, start_scheduler
@@ -25,9 +27,51 @@ logging.basicConfig(
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def _bootstrap_db() -> None:
"""첫 부팅 자동화:
1) migrations/*.sql idempotent 적용 (timescale/pgvector 확장 + 스키마)
2) symbols 테이블 비어있으면 pykrx 로 전 종목 시드 (SEED 10 마크 포함)
BOOTSTRAP_DISABLED=1 이면 스킵 (테스트/CI 용). 어떤 단계든 실패해도 서버는
뜬다 — /health/db 가 진단을 알려준다.
"""
if os.environ.get("BOOTSTRAP_DISABLED") == "1":
logger.info("bootstrap skipped (BOOTSTRAP_DISABLED=1)")
return
# 1) migrations
try:
from app.db.migrate import apply_all
res = apply_all()
logger.info("bootstrap migrate: %s", res)
except Exception: # noqa: BLE001
logger.exception("bootstrap migrate failed")
return # 스키마 없으면 시드 불가
# 2) symbols 시드 (비어있을 때만 — pykrx 호출이 비싸므로 항상 돌리지 않음)
try:
eng = get_engine()
with eng.connect() as conn:
row = conn.execute(text("SELECT COUNT(*) FROM symbols")).first()
count = int(row[0]) if row else 0
if count == 0:
logger.info("symbols empty — running initial seed")
from app.fetch.symbols_seed import seed_symbols
report = seed_symbols()
logger.info("bootstrap seed_symbols: %s", report)
else:
logger.info("symbols already populated (count=%d) — skip seed", count)
except Exception: # noqa: BLE001
logger.exception("bootstrap seed_symbols failed")
@asynccontextmanager @asynccontextmanager
async def lifespan(_: FastAPI): async def lifespan(_: FastAPI):
_bootstrap_db()
# 스케줄러는 옵션. CI/테스트에서 disable 하고 싶으면 SCHEDULER_DISABLED 같은 env 추가 가능. # 스케줄러는 옵션. CI/테스트에서 disable 하고 싶으면 SCHEDULER_DISABLED 같은 env 추가 가능.
if os.environ.get("SCHEDULER_DISABLED") == "1":
logger.info("scheduler skipped (SCHEDULER_DISABLED=1)")
else:
try: try:
start_scheduler() start_scheduler()
except Exception: # noqa: BLE001 except Exception: # noqa: BLE001