Translaterpeed / app /database /connection.py
Ruhivig65's picture
Upload 4 files
b8907ef verified
"""
============================================
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.")