feat: Migrate batch analysis DB writes to db_worker queue pattern
This commit is contained in:
@@ -1,48 +1,102 @@
|
||||
"""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, StockAnalysis
|
||||
from app.models.models import TradeHistory
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 전역(Global) 비동기 큐 생성 (메모리 버퍼 역할)
|
||||
db_task_queue = asyncio.Queue()
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 전역 큐 — 앱 어디서나 import 하여 put() 호출
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
db_task_queue: asyncio.Queue = asyncio.Queue()
|
||||
|
||||
async def db_writer_worker():
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 편의 함수 — 호출 측에서 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 을 자동 관리한다.
|
||||
"""
|
||||
메인 스레드나 API 스레드를 막지 않고,
|
||||
백그라운드에서 큐에 쌓인 DB 작업만 순차적으로 처리하는 워커
|
||||
DB 커넥션 경합과 락(Lock)을 방지합니다.
|
||||
"""
|
||||
logger.info("🛠️ Background DB Writer Worker started")
|
||||
logger.info("🛠️ Background DB Writer Worker started")
|
||||
while True:
|
||||
task = await db_task_queue.get()
|
||||
try:
|
||||
# 큐에 데이터가 들어올 때까지 대기
|
||||
task = await db_task_queue.get()
|
||||
task_type = task.get("type")
|
||||
data = task.get("data")
|
||||
|
||||
# 워커 전용 DB 세션을 열어서 처리 (락 경합 최소화)
|
||||
async with AsyncSessionLocal() as session:
|
||||
# ── 1. 매매일지 INSERT ─────────────────────────────────────
|
||||
if task_type == "insert_trade_history":
|
||||
# 매매일지 추가
|
||||
new_trade = TradeHistory(**data)
|
||||
session.add(new_trade)
|
||||
session.add(TradeHistory(**data))
|
||||
await session.commit()
|
||||
logger.info(f"✅ [Worker] 매매일지 비동기 저장 완료: {data.get('stock_code', 'Unknown')}")
|
||||
logger.info(
|
||||
"✅ [Worker] TradeHistory saved: %s",
|
||||
data.get("stock_code", "?"),
|
||||
)
|
||||
|
||||
# ── 2. 단건 StockAnalysis UPSERT ──────────────────────────
|
||||
elif task_type == "upsert_stock_analysis":
|
||||
# 종목 분석 결과 저장 (추후 batch_analysis 등에서 이관될 로직용 플레이스홀더)
|
||||
# 실제 upsert 로직은 데이터 구조에 맞춰 구현
|
||||
logger.info(f"✅ [Worker] 종목분석 비동기 저장 처리 (Placeholder): {data.get('stock_code', 'Unknown')}")
|
||||
|
||||
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(f"⚠️ [Worker] 알 수 없는 작업 타입: {task_type}")
|
||||
logger.warning("⚠️ [Worker] Unknown task type: %s", task_type)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
logger.info("⏹️ DB Writer Worker stopped")
|
||||
logger.info("⏹️ DB Writer Worker stopped")
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"❌ [Worker] DB 작업 중 에러 발생: {e}", exc_info=True)
|
||||
except Exception as exc:
|
||||
logger.error("❌ [Worker] DB task failed: %s", exc, exc_info=True)
|
||||
# 실패한 task 는 드롭 — 재시도가 필요하면 retry_count 추가 가능
|
||||
finally:
|
||||
db_task_queue.task_done()
|
||||
|
||||
Reference in New Issue
Block a user