140 lines
4 KiB
Python
140 lines
4 KiB
Python
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
|
from sqlalchemy.pool import NullPool
|
|
from sqlalchemy import event, text
|
|
import logging
|
|
|
|
from config.config import settings
|
|
from database.models.generic_model import Base
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
DATABASE_URL = settings.database_url
|
|
|
|
|
|
def _configure_sqlite_connection(dbapi_connection, connection_record):
|
|
cursor = dbapi_connection.cursor()
|
|
cursor.execute("PRAGMA journal_mode=WAL")
|
|
cursor.execute("PRAGMA busy_timeout=30000")
|
|
cursor.execute("PRAGMA synchronous=NORMAL")
|
|
cursor.execute("PRAGMA cache_size=-64000") # 64MB
|
|
cursor.execute("PRAGMA foreign_keys=ON")
|
|
cursor.execute("PRAGMA locking_mode=NORMAL")
|
|
cursor.close()
|
|
|
|
logger.debug("SQLite configuré avec WAL mode et busy_timeout=30s")
|
|
|
|
|
|
engine_kwargs = {
|
|
"echo": False,
|
|
"future": True,
|
|
"poolclass": NullPool,
|
|
}
|
|
|
|
if DATABASE_URL and "sqlite" in DATABASE_URL:
|
|
engine_kwargs["connect_args"] = {
|
|
"check_same_thread": False,
|
|
"timeout": 30,
|
|
}
|
|
|
|
engine = create_async_engine(DATABASE_URL, **engine_kwargs)
|
|
|
|
if DATABASE_URL and "sqlite" in DATABASE_URL:
|
|
|
|
@event.listens_for(engine.sync_engine, "connect")
|
|
def set_sqlite_pragma(dbapi_connection, connection_record):
|
|
_configure_sqlite_connection(dbapi_connection, connection_record)
|
|
|
|
|
|
async_session_factory = async_sessionmaker(
|
|
engine,
|
|
class_=AsyncSession,
|
|
expire_on_commit=False,
|
|
autoflush=False,
|
|
)
|
|
|
|
|
|
async def init_db():
|
|
logger.info("Debut init_db")
|
|
try:
|
|
logger.info("Tentative de connexion")
|
|
async with engine.begin() as conn:
|
|
logger.info("Connexion etablie")
|
|
|
|
if DATABASE_URL and "sqlite" in DATABASE_URL:
|
|
result = await conn.execute(text("PRAGMA journal_mode"))
|
|
journal_mode = result.scalar()
|
|
logger.info(f"SQLite journal_mode: {journal_mode}")
|
|
|
|
await conn.run_sync(Base.metadata.create_all)
|
|
logger.info("create_all execute")
|
|
|
|
logger.info("Base de données initialisée avec succès")
|
|
logger.info(f"Fichier DB: {DATABASE_URL}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Erreur initialisation DB: {e}")
|
|
raise
|
|
|
|
|
|
async def get_session() -> AsyncSession:
|
|
async with async_session_factory() as session:
|
|
yield session
|
|
|
|
|
|
async def close_db():
|
|
await engine.dispose()
|
|
logger.info("Connexions DB fermées")
|
|
|
|
|
|
async def execute_with_sqlite_retry(
|
|
session: AsyncSession, statement, max_retries: int = 5, base_delay: float = 0.1
|
|
):
|
|
import asyncio
|
|
from sqlalchemy.exc import OperationalError
|
|
|
|
last_error = None
|
|
|
|
for attempt in range(max_retries):
|
|
try:
|
|
result = await session.execute(statement)
|
|
return result
|
|
except OperationalError as e:
|
|
last_error = e
|
|
if "database is locked" in str(e).lower():
|
|
delay = base_delay * (2**attempt)
|
|
logger.warning(
|
|
f"SQLite locked, tentative {attempt + 1}/{max_retries}, "
|
|
f"retry dans {delay:.2f}s"
|
|
)
|
|
await asyncio.sleep(delay)
|
|
else:
|
|
raise
|
|
|
|
raise last_error
|
|
|
|
|
|
async def commit_with_retry(
|
|
session: AsyncSession, max_retries: int = 5, base_delay: float = 0.1
|
|
):
|
|
import asyncio
|
|
from sqlalchemy.exc import OperationalError
|
|
|
|
last_error = None
|
|
|
|
for attempt in range(max_retries):
|
|
try:
|
|
await session.commit()
|
|
return
|
|
except OperationalError as e:
|
|
last_error = e
|
|
if "database is locked" in str(e).lower():
|
|
delay = base_delay * (2**attempt)
|
|
logger.warning(
|
|
f"SQLite locked lors du commit, tentative {attempt + 1}/{max_retries}, "
|
|
f"retry dans {delay:.2f}s"
|
|
)
|
|
await asyncio.sleep(delay)
|
|
else:
|
|
raise
|
|
|
|
raise last_error
|