US07-04: Prove Concurrency and Crash Recovery (#86)

This commit was merged in pull request #86.
This commit is contained in:
2026-08-17 11:09:29 +02:00
parent 3fa35fe21e
commit d08d19c03c
21 changed files with 1740 additions and 40 deletions

View File

@@ -78,6 +78,11 @@ class AnalysisService:
latest[review.asset_id] = review.decision
return {aid for aid, decision in latest.items() if decision == SFW}
def _is_still_sfw(self, asset_id: str) -> bool:
"""Re-read the current safety decision straight from the database."""
with self._session_factory() as session:
return asset_id in self._sfw_asset_ids(session)
def eligible_asset_ids(self) -> list[str]:
"""Confirmed-SFW canonical active assets without a completed analysis."""
with self._session_factory() as session:
@@ -161,6 +166,22 @@ class AnalysisService:
self._store(asset_id, status="error", result=None, error=str(error), tokens=0, raw="")
errors += 1
continue
# Third gate, after the call: a provider request takes seconds, and the
# reviewer may have flipped this asset to NSFW while it was in flight.
# The result describes an asset that is no longer analysable, so it is
# discarded — not stored, and above all not written into its EXIF
# (concept §18 scenario 7, US07-04).
if not self._is_still_sfw(asset_id):
self._store(
asset_id,
status="skipped_nsfw",
result=None,
error="the safety decision changed while analysis was in flight",
tokens=0,
raw="",
)
skipped += 1
continue
self._store(
asset_id,
status="analyzed",