fix(eu-umbau): SQLite-Parallelitaet der Doppelspur abgesichert

busy_timeout von 5s auf 30s (zwei gleichzeitig schreibende Lanes) und
'database is locked' im Orchestrator als transienter Fehler mit Retry
statt als permanenter Abbruch. Gefunden im Doppelspur-Paralleltest
(Lage-Summary-Update kollidierte mit der Schreiblast der zweiten Lane).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dieser Commit ist enthalten in:
claude-dev
2026-07-30 20:39:41 +00:00
Ursprung 5fffca614a
Commit 676741b1c4
2 geänderte Dateien mit 12 neuen und 2 gelöschten Zeilen

Datei anzeigen

@@ -616,6 +616,13 @@ class AgentOrchestrator:
def _is_transient_cli(err: Exception) -> bool: def _is_transient_cli(err: Exception) -> bool:
return isinstance(err, ClaudeCliError) and err.error_type in ("rate_limit", "timeout") return isinstance(err, ClaudeCliError) and err.error_type in ("rate_limit", "timeout")
def _is_db_locked(err: Exception) -> bool:
# Seit der Doppelspur (Phase 2) koennen sich parallele Lanes beim
# Schreiben auf die SQLite-DB kurz blockieren. Das ist transient
# und einen Retry wert, kein permanenter Fehler.
import sqlite3 as _sqlite3
return isinstance(err, _sqlite3.OperationalError) and "locked" in str(err).lower()
# Optionales globales Ventil (Default aus): begrenzt gleichzeitige Recherchen. # Optionales globales Ventil (Default aus): begrenzt gleichzeitige Recherchen.
sem = self._global_sem sem = self._global_sem
if sem is not None: if sem is not None:
@@ -657,7 +664,7 @@ class AgentOrchestrator:
break break
# Transiente Fehler: Retry bis 3x # Transiente Fehler: Retry bis 3x
if isinstance(e, TRANSIENT_ERRORS) or _is_transient_cli(e): if isinstance(e, TRANSIENT_ERRORS) or _is_transient_cli(e) or _is_db_locked(e):
last_error = e last_error = e
kind = e.error_type if isinstance(e, ClaudeCliError) else type(e).__name__ kind = e.error_type if isinstance(e, ClaudeCliError) else type(e).__name__
logger.warning(f"Transienter Fehler [{kind}] bei Lage {incident_id} (Versuch {attempt + 1}/3): {e}") logger.warning(f"Transienter Fehler [{kind}] bei Lage {incident_id} (Versuch {attempt + 1}/3): {e}")

Datei anzeigen

@@ -392,7 +392,10 @@ async def get_db() -> aiosqlite.Connection:
db.row_factory = aiosqlite.Row db.row_factory = aiosqlite.Row
await db.execute("PRAGMA journal_mode=WAL") await db.execute("PRAGMA journal_mode=WAL")
await db.execute("PRAGMA foreign_keys=ON") await db.execute("PRAGMA foreign_keys=ON")
await db.execute("PRAGMA busy_timeout=5000") # 30s statt 5s seit der Doppelspur (EU-Umbau Phase 2). Zwei gleichzeitig
# laufende Refreshes derselben Organisation schreiben parallel in die DB,
# 5s reichten dabei nicht immer (database is locked beim Summary-Update).
await db.execute("PRAGMA busy_timeout=30000")
return db return db