Spaces:
Sleeping
Sleeping
| """ | |
| ============================================ | |
| Database Connection Manager | |
| - Async SQLAlchemy Engine with Connection Pooling | |
| - Handles Render's free tier quirks: | |
| * Sleep mode reconnection | |
| * Connection drop recovery | |
| * Pool pre-ping for stale connections | |
| ============================================ | |
| """ | |
| import logging | |
| from sqlalchemy.ext.asyncio import ( | |
| create_async_engine, | |
| AsyncSession, | |
| async_sessionmaker, | |
| AsyncEngine, | |
| ) | |
| from sqlalchemy.orm import DeclarativeBase | |
| from sqlalchemy.pool import AsyncAdaptedQueuePool | |
| from sqlalchemy import text | |
| from app.config import get_database_url, settings | |
| logger = logging.getLogger(__name__) | |
| # ============================================ | |
| # Base Model Class (all models inherit this) | |
| # ============================================ | |
| class Base(DeclarativeBase): | |
| """Base class for all database models.""" | |
| pass | |
| # ============================================ | |
| # Engine Configuration | |
| # ============================================ | |
| _engine: AsyncEngine | None = None | |
| _session_factory: async_sessionmaker[AsyncSession] | None = None | |
| def get_engine() -> AsyncEngine: | |
| """ | |
| Create or return the async SQLAlchemy engine. | |
| Key settings for Render Free Tier: | |
| - pool_pre_ping=True: Tests connection before using it | |
| (prevents "connection closed" errors after DB sleep) | |
| - pool_size=5: Small pool (free tier has limited connections) | |
| - max_overflow=3: Allow 3 extra connections under load | |
| - pool_recycle=300: Recycle connections every 5 minutes | |
| (prevents stale connections from Render's idle timeout) | |
| - pool_timeout=30: Wait max 30s for a connection from pool | |
| """ | |
| global _engine | |
| if _engine is None: | |
| database_url = get_database_url() | |
| logger.info("Creating database engine...") | |
| logger.info(f"Database host: {database_url.split('@')[1].split('/')[0] if '@' in database_url else 'unknown'}") | |
| _engine = create_async_engine( | |
| database_url, | |
| # --- Connection Pool Settings --- | |
| poolclass=AsyncAdaptedQueuePool, | |
| pool_pre_ping=True, # CRITICAL: Check connection health | |
| pool_size=5, # Base connections in pool | |
| max_overflow=3, # Extra connections allowed | |
| pool_recycle=300, # Recycle every 5 min (300 sec) | |
| pool_timeout=30, # Wait 30s for available connection | |
| # --- Query Settings --- | |
| echo=settings.DEBUG, # Log SQL queries in debug mode | |
| # --- Connection Arguments --- | |
| connect_args={ | |
| "timeout": 30, # Connection timeout | |
| "command_timeout": 60, # Query timeout | |
| "server_settings": { | |
| "application_name": "novel_scraper", | |
| "idle_in_transaction_session_timeout": "60000", # 60s | |
| }, | |
| }, | |
| ) | |
| logger.info("Database engine created successfully.") | |
| return _engine | |
| def get_session_factory() -> async_sessionmaker[AsyncSession]: | |
| """ | |
| Create or return the async session factory. | |
| """ | |
| global _session_factory | |
| if _session_factory is None: | |
| engine = get_engine() | |
| _session_factory = async_sessionmaker( | |
| bind=engine, | |
| class_=AsyncSession, | |
| expire_on_commit=False, # Don't expire objects after commit | |
| autocommit=False, | |
| autoflush=False, | |
| ) | |
| logger.info("Session factory created.") | |
| return _session_factory | |
| async def get_db_session() -> AsyncSession: | |
| """ | |
| Dependency for FastAPI routes. | |
| Yields an async database session. | |
| Usage in FastAPI: | |
| @router.get("/something") | |
| async def my_route(db: AsyncSession = Depends(get_db_session)): | |
| ... | |
| """ | |
| factory = get_session_factory() | |
| async with factory() as session: | |
| try: | |
| yield session | |
| await session.commit() | |
| except Exception as e: | |
| await session.rollback() | |
| logger.error(f"Database session error: {e}") | |
| raise | |
| finally: | |
| await session.close() | |
| async def init_database(): | |
| """ | |
| Initialize database: Create all tables. | |
| Called on application startup. | |
| """ | |
| engine = get_engine() | |
| try: | |
| # Test connection first | |
| async with engine.begin() as conn: | |
| await conn.execute(text("SELECT 1")) | |
| logger.info("✅ Database connection test successful!") | |
| # Create all tables | |
| async with engine.begin() as conn: | |
| await conn.run_sync(Base.metadata.create_all) | |
| logger.info("✅ Database tables created/verified!") | |
| except Exception as e: | |
| logger.error(f"❌ Database initialization failed: {e}") | |
| logger.error("Check your DATABASE_URL environment variable!") | |
| raise | |
| async def close_database(): | |
| """ | |
| Close database connections. | |
| Called on application shutdown. | |
| """ | |
| global _engine, _session_factory | |
| if _engine is not None: | |
| await _engine.dispose() | |
| _engine = None | |
| _session_factory = None | |
| logger.info("Database connections closed.") |