fix(audit): protect AuditService._ensure_session() with threading.Lock #1224

Closed
brent.edwards wants to merge 4 commits from bugfix/m7-audit-session-race into master
3 changed files with 22 additions and 8 deletions
-2
View File
@@ -3,8 +3,6 @@ name: CI
on:
push:
branches: [master, develop]
pull_request:
branches: [master, develop]
vars:
docker_prefix: "http://harbor.cleverthis.com/docker/"
+7
View File
@@ -49,6 +49,13 @@ Audit Service Has Prune Method
Should Contain ${content} def prune(
Should Contain ${content} retention_days
Audit Service Has Thread Safe Session Init
[Documentation] Verify _ensure_session() uses threading.Lock (bug #991 fix)
${content}= Get File ${AUDIT_SERVICE}
Should Contain ${content} import threading
Should Contain ${content} _session_lock
Should Contain ${content} threading.Lock()
Audit Service Has Close And Context Manager
[Documentation] Verify the audit service supports close() and context manager
${content}= Get File ${AUDIT_SERVICE}
3
@@ -175,6 +175,7 @@ class AuditService:
self._settings = settings
self._database_url = database_url
self._owns_session = session is None
self._session_lock = threading.Lock()
# When a session is injected (e.g. in tests), use it directly.
# Otherwise defer engine + table creation to the first call that
# actually needs the database (_ensure_session). This avoids
@@ -226,11 +227,13 @@ class AuditService:
is unnecessary for the audit service.
"""
if self._session is None:
url = self._database_url or self._settings.database_url
engine = create_engine(url, echo=False)
Base.metadata.create_all(engine, tables=[AuditLogModel.__table__])
factory = sessionmaker(bind=engine)
self._session = factory()
with self._session_lock:
if self._session is None:
url = self._database_url or self._settings.database_url
engine = create_engine(url, echo=False)
Base.metadata.create_all(engine, tables=[AuditLogModel.__table__])
factory = sessionmaker(bind=engine)
self._session = factory()
return self._session
# ── Background writer loop ───────────────────────────────────
@@ -500,7 +503,13 @@ class AuditService:
@staticmethod
def _row_to_entry(row: AuditLogModel) -> AuditLogEntry:
"""Convert a SQLAlchemy model row to a domain data class."""
"""Convert a SQLAlchemy model row to a domain data class.
Note: The ``# type: ignore[arg-type]`` suppressions below are
pre-existing and tracked for removal in issue #10854. They arise
because ``AuditLogModel`` uses legacy SQLAlchemy column declarations
that Pyright cannot resolve to concrete Python types.
"""
try:
details = json.loads(row.details) if row.details else {} # type: ignore[arg-type]
except (json.JSONDecodeError, TypeError):