fix: release cooldown on local media failures
CI / backend-and-migrations (push) Failing after 2m26s
CI / astro-build (push) Failing after 37s
CI / dependency-audit (push) Failing after 31s
CI / compose-e2e (push) Failing after 40s

This commit is contained in:
ik
2026-09-22 20:01:20 +07:00
parent 96c29ab9d9
commit 1920475580
3 changed files with 38 additions and 4 deletions
+17 -2
View File
@@ -7,7 +7,7 @@ from pathlib import Path
import urllib.error
import urllib.request
from .community_cli import MIN_FETCH_INTERVAL_SECONDS, USER_AGENT, _StrictRedirectHandler, _read_state, _validate_url_before_io, check_and_reserve, fetch_html, fetch_site_key
from .community_cli import MIN_FETCH_INTERVAL_SECONDS, USER_AGENT, _StrictRedirectHandler, _read_state, _validate_url_before_io, check_and_reserve, fetch_html, fetch_site_key, release_reservation
from .media_assets import approve_stored_assets, audit_media_catalog, compare_quality_upgrades, extract_media_candidates, generate_media_derivatives, generate_quality_contact_sheets, media_coverage, media_quality_report, merge_manifest, publish_quality_upgrades, queue_quality_upgrades, reconcile_queued_duplicates, reclassify_manifest, review_asset, rollback_quality_upgrade, store_asset
@@ -104,6 +104,11 @@ def _attempt_asset(root: Path, manifest_path: Path, manifest: dict, queued: dict
return f"stored {url} as {relative}", None
def _response_was_received(error: Exception) -> bool:
"""Only an HTTP response or validated body is allowed to consume cooldown."""
return isinstance(error, (urllib.error.HTTPError, ValueError))
def _download_one(root: Path, state_file: Path, asset_url: str | None = None) -> str:
manifest_path = root / "manifest.json"
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
@@ -112,9 +117,11 @@ def _download_one(root: Path, state_file: Path, asset_url: str | None = None) ->
return "queue is empty" if asset_url is None else "asset is not queued"
url = queued["asset_url"]
_validate_url_before_io(url)
check_and_reserve(fetch_site_key(url), state_file=state_file)
reserved_at = check_and_reserve(fetch_site_key(url), state_file=state_file)
message, error = _attempt_asset(root, manifest_path, manifest, queued)
if error is not None:
if not _response_was_received(error):
release_reservation(fetch_site_key(url), state_file=state_file, reserved_at=reserved_at)
raise error
return message
@@ -142,6 +149,7 @@ def _download_batch(root: Path, state_file: Path, *, limit: int = MAX_BATCH_ASSE
domains[site] = {"attempted": 0, "stored": 0, "failed": 0, "skipped": str(exc)}
continue
stored = failed = consecutive_failures = 0
response_received = False
messages: list[str] = []
stopped_reason: str | None = None
for item in items[:limit]:
@@ -149,17 +157,24 @@ def _download_batch(root: Path, state_file: Path, *, limit: int = MAX_BATCH_ASSE
messages.append(message)
if error is None:
stored += 1
response_received = True
consecutive_failures = 0
continue
failed += 1
consecutive_failures += 1
status = item.get("status")
if _response_was_received(error):
response_received = True
if status in {"blocked", "queued"}:
stopped_reason = f"domain stopped after {status} response"
break
if consecutive_failures >= MAX_CONSECUTIVE_FAILURES:
stopped_reason = f"domain stopped after {MAX_CONSECUTIVE_FAILURES} consecutive failures"
break
if not response_received and failed and stored == 0:
# No HTTP response was observed in this reservation window. It is
# safe to release this process's reservation for a later retry.
release_reservation(site, state_file=state_file, reserved_at=reserved_at)
domains[site] = {
"attempted": stored + failed, "stored": stored, "failed": failed,
"stopped_reason": stopped_reason, "messages": messages,