# app/main.py import asyncio from contextlib import asynccontextmanager from fastapi import FastAPI from app.utils.logger import get_logger from app.utils.nvMapPwScraper import NvMapPwScraper from config import ssulbox_settings logger = get_logger("core") @asynccontextmanager async def lifespan(app: FastAPI): """FastAPI 애플리케이션 생명주기 관리""" # Startup - 애플리케이션 시작 시 logger.info("Starting up...") try: from config import prj_settings # DEBUG 모드일 때만 데이터베이스 테이블 자동 생성 if prj_settings.DEBUG: from app.database.session import create_db_tables await create_db_tables() logger.info("Database tables created (DEBUG mode)") # dashboard 테이블 초기화 및 기존 데이터 마이그레이션 (모든 환경) from app.dashboard.migration import init_dashboard_table await init_dashboard_table() # 썰박스 스키마는 앱이 만들지 않는다 — **DB 변경은 전부 수동**이 방침이다. # 배포 전에 docs/manual_ddl/2026-07-30-ssulbox.sql 을 직접 실행할 것. # (자동 마이그레이션 ensure_ssulbox_schema() 는 2026-07-30 제거) if ssulbox_settings.SSULBOX_ENABLED: # 고아 잡 스윕 — 이전 프로세스가 죽으며 남긴 queued/running 을 환불·정리. # 이건 DDL 이 아니라 데이터 정리(환불 UPDATE)라 앱 책임으로 남긴다. # 기동 직후 인메모리 잡은 0개이므로 비터미널 잡은 전부 고아다. # ⚠️ 이 불변식은 단일 워커 전제다(--workers 를 늘리면 정상 잡을 오판한다). try: from app.database.session import BackgroundSessionLocal from app.ssulbox.services import task_service async with BackgroundSessionLocal() as session: swept = await task_service.sweep_orphans(session) await session.commit() if swept: logger.info(f"[ssulbox] 고아 잡 {swept}건 환불·정리") except Exception as e: logger.error( f"[ssulbox] 고아 스윕 실패 (다음 기동 시 재시도): " f"{type(e).__name__}: {e}" ) await NvMapPwScraper.initiate_scraper() except asyncio.TimeoutError: logger.error("Database initialization timed out") # 타임아웃 시 앱 시작 중단하려면 raise, 계속하려면 pass raise except Exception as e: logger.error(f"Database initialization failed: {e}") # 에러 시 앱 시작 중단하려면 raise, 계속하려면 pass raise yield # 애플리케이션 실행 중 # Shutdown - 애플리케이션 종료 시 logger.info("Shutting down...") # 썰박스 신규 잡 큐잉 차단. dispose_engine() 전에 해야 워커 스레드가 # 죽은 커넥션에 접근하지 않는다. 진행 중이던 잡은 다음 기동의 스윕이 환불한다. if ssulbox_settings.SSULBOX_ENABLED: try: from app.ssulbox.worker import job_manager job_manager.shutdown() except Exception as e: logger.warning(f"[ssulbox] 종료 처리 실패: {e}") # 공유 HTTP 클라이언트 종료 from app.utils.creatomate import close_shared_client from app.utils.upload_blob_as_request import close_shared_blob_client await close_shared_client() await close_shared_blob_client() from app.database.like_cache import close_like_cache await close_like_cache() # 데이터베이스 엔진 종료 from app.database.session import dispose_engine await dispose_engine() # FastAPI 앱 생성 (lifespan 적용) app = FastAPI(title="CastAD", lifespan=lifespan)