"""Background DB Writer Worker ARCHITECTURE (CLAUDE.md 3.2): - 실시간/콜백 스레드에서는 절대 DB 세션을 직접 열지 않는다. - 모든 DB 쓰기 작업은 이 모듈의 db_task_queue 로 위임한다. - 워커가 큐를 소비하며 단일 세션에서 순차 처리 → 락 경합 제거. Supported task types: - insert_trade_history : dict → TradeHistory INSERT - upsert_stock_analysis : dict → StockAnalysis single UPSERT - bulk_upsert_stock_analysis : list → StockAnalysis batch UPSERT """ import asyncio import logging from datetime import date from app.core.database import AsyncSessionLocal from app.models.models import TradeHistory logger = logging.getLogger(__name__) # ───────────────────────────────────────────────────────────────────────────── # 전역 큐 — 앱 어디서나 import 하여 put() 호출 # ───────────────────────────────────────────────────────────────────────────── db_task_queue: asyncio.Queue = asyncio.Queue() # ───────────────────────────────────────────────────────────────────────────── # 편의 함수 — 호출 측에서 dict 구성 없이 바로 사용 # ───────────────────────────────────────────────────────────────────────────── async def enqueue_trade_history(data: dict) -> None: """매매일지 INSERT 를 큐에 등록.""" await db_task_queue.put({"type": "insert_trade_history", "data": data}) async def enqueue_upsert_analysis(row: dict) -> None: """단건 StockAnalysis UPSERT 를 큐에 등록.""" await db_task_queue.put({"type": "upsert_stock_analysis", "data": row}) async def enqueue_bulk_upsert_analysis(rows: list[dict]) -> None: """다건 StockAnalysis batch UPSERT 를 큐에 등록.""" if rows: await db_task_queue.put({"type": "bulk_upsert_stock_analysis", "data": rows}) # ───────────────────────────────────────────────────────────────────────────── # 워커 본체 # ───────────────────────────────────────────────────────────────────────────── async def db_writer_worker() -> None: """lifespan 에서 asyncio.create_task() 로 실행되는 영구 백그라운드 워커. 큐에서 task 를 꺼내 단일 AsyncSession 안에서 처리한 뒤 commit / rollback 을 자동 관리한다. """ logger.info("🛠️ Background DB Writer Worker started") while True: task = await db_task_queue.get() try: task_type = task.get("type") data = task.get("data") async with AsyncSessionLocal() as session: # ── 1. 매매일지 INSERT ───────────────────────────────────── if task_type == "insert_trade_history": session.add(TradeHistory(**data)) await session.commit() logger.info( "✅ [Worker] TradeHistory saved: %s", data.get("stock_code", "?"), ) # ── 2. 단건 StockAnalysis UPSERT ────────────────────────── elif task_type == "upsert_stock_analysis": from app.services.analysis_db_service import _execute_upsert await _execute_upsert(session, [data]) await session.commit() logger.debug( "✅ [Worker] StockAnalysis upsert: %s", data.get("stock_code", "?"), ) # ── 3. 다건 StockAnalysis batch UPSERT ──────────────────── elif task_type == "bulk_upsert_stock_analysis": from app.services.analysis_db_service import _execute_upsert await _execute_upsert(session, data) # data = list[dict] await session.commit() logger.info( "✅ [Worker] Bulk StockAnalysis upsert: %d rows", len(data) ) else: logger.warning("⚠️ [Worker] Unknown task type: %s", task_type) except asyncio.CancelledError: logger.info("⏹️ DB Writer Worker stopped") break except Exception as exc: logger.error("❌ [Worker] DB task failed: %s", exc, exc_info=True) # 실패한 task 는 드롭 — 재시도가 필요하면 retry_count 추가 가능 finally: db_task_queue.task_done()