更新
This commit is contained in:
@@ -9,8 +9,10 @@ from typing import Any
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy import event, text
|
||||
from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
|
||||
from sqlalchemy.engine import make_url
|
||||
from sqlalchemy.pool import AsyncAdaptedQueuePool, StaticPool
|
||||
|
||||
BACKEND_DIR = Path(__file__).resolve().parent.parent
|
||||
PROJECT_ROOT = BACKEND_DIR.parent
|
||||
@@ -37,6 +39,10 @@ def _env_int(name: str, default: int) -> int:
|
||||
return default
|
||||
|
||||
|
||||
def _bounded_env_int(name: str, default: int, minimum: int, maximum: int) -> int:
|
||||
return max(minimum, min(maximum, _env_int(name, default)))
|
||||
|
||||
|
||||
def normalize_db_type(value: str | None) -> str:
|
||||
raw = (value or "sqlite").strip().lower()
|
||||
if raw in ("postgres", "pgsql"):
|
||||
@@ -136,7 +142,35 @@ def build_database_url(config: DatabaseConfig | None = None) -> str:
|
||||
|
||||
def engine_kwargs_for_url(url: str) -> dict[str, Any]:
|
||||
kwargs: dict[str, Any] = {"echo": False}
|
||||
if not url.startswith("sqlite"):
|
||||
if url.startswith("sqlite"):
|
||||
# SQLAlchemy 2.0.30 defaults file-backed aiosqlite to NullPool. At
|
||||
# hundreds of hosted accounts that creates and tears down an aiosqlite
|
||||
# worker thread for every short query. Reuse a small bounded pool;
|
||||
# SQLite still serializes writers, so a large pool only adds lock
|
||||
# contention and does not improve throughput.
|
||||
busy_timeout_ms = _bounded_env_int(
|
||||
"KEFU_SQLITE_BUSY_TIMEOUT_MS", 30_000, 1_000, 120_000
|
||||
)
|
||||
kwargs["connect_args"] = {"timeout": busy_timeout_ms / 1000.0}
|
||||
sqlite_database = make_url(url).database
|
||||
is_memory_database = (
|
||||
not sqlite_database
|
||||
or sqlite_database == ":memory:"
|
||||
or "mode=memory" in url.lower()
|
||||
)
|
||||
if is_memory_database:
|
||||
# Every connection to an in-memory SQLite URL otherwise receives a
|
||||
# different database. StaticPool preserves the single shared
|
||||
# connection expected by tests and utility callers.
|
||||
kwargs["poolclass"] = StaticPool
|
||||
else:
|
||||
kwargs["poolclass"] = AsyncAdaptedQueuePool
|
||||
kwargs["pool_size"] = _bounded_env_int(
|
||||
"KEFU_SQLITE_POOL_SIZE", 5, 1, 10
|
||||
)
|
||||
kwargs["max_overflow"] = 0
|
||||
kwargs["pool_timeout"] = max(5.0, busy_timeout_ms / 1000.0)
|
||||
else:
|
||||
kwargs["pool_pre_ping"] = True
|
||||
kwargs["pool_recycle"] = 3600
|
||||
# 默认连接池仅 pool_size=5 + max_overflow=10。多账号托管 + 前端并发请求时
|
||||
@@ -155,9 +189,33 @@ def engine_kwargs_for_url(url: str) -> dict[str, Any]:
|
||||
return kwargs
|
||||
|
||||
|
||||
def _configure_sqlite_connection(dbapi_connection, _connection_record) -> None:
|
||||
"""Apply process-wide SQLite settings to every pooled connection.
|
||||
|
||||
WAL lets readers continue while the single writer commits. NORMAL avoids
|
||||
a full disk sync for every small log/status transaction while retaining
|
||||
WAL crash consistency. busy_timeout turns transient writer contention
|
||||
into bounded waiting instead of immediate ``database is locked`` errors.
|
||||
"""
|
||||
busy_timeout_ms = _bounded_env_int(
|
||||
"KEFU_SQLITE_BUSY_TIMEOUT_MS", 30_000, 1_000, 120_000
|
||||
)
|
||||
cursor = dbapi_connection.cursor()
|
||||
try:
|
||||
cursor.execute(f"PRAGMA busy_timeout={busy_timeout_ms}")
|
||||
cursor.execute("PRAGMA journal_mode=WAL")
|
||||
cursor.execute("PRAGMA synchronous=NORMAL")
|
||||
cursor.execute("PRAGMA foreign_keys=ON")
|
||||
finally:
|
||||
cursor.close()
|
||||
|
||||
|
||||
def create_database_engine(config: DatabaseConfig | None = None) -> AsyncEngine:
|
||||
url = build_database_url(config)
|
||||
return create_async_engine(url, **engine_kwargs_for_url(url))
|
||||
engine = create_async_engine(url, **engine_kwargs_for_url(url))
|
||||
if url.startswith("sqlite"):
|
||||
event.listen(engine.sync_engine, "connect", _configure_sqlite_connection)
|
||||
return engine
|
||||
|
||||
|
||||
def mask_database_url(url: str) -> str:
|
||||
|
||||
Reference in New Issue
Block a user