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:
@@ -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}")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren