[P1][BUG] DB-Connection-Pool nicht konfiguriert (Default 5+10) — Pool-Erschöpfung bei mehreren Nodes #227

Open
opened 2026-07-27 10:44:21 +00:00 by chinux · 1 comment
Owner

Blockiert das Anbinden weiterer Produktiv-Nodes.

Befund

server/database.py:15:

engine = create_async_engine(DATABASE_URL, echo=False, pool_pre_ping=True)

Weder pool_size noch max_overflow gesetzt. SQLAlchemy-Defaults: pool_size=5, max_overflow=10maximal 15 gleichzeitige Verbindungen für das gesamte Backend.

Auswirkung

Der Verbrauch skaliert mit der Node-Zahl. websocket/agent_ws.py öffnet an 12 Stellen eine eigene AsyncSessionLocal(), überwiegend im Nachrichten-Handler — also pro eingehender Agent-Nachricht. Bei scan_interval von 300 s (Default, models/node.py:53) plus Heartbeats, Log-Batches, Task-Updates und Tunnel-Events erzeugt jeder Node dauerhaft Sessions. Dazu die Request-Sessions des Frontends und je 9 Stellen im Scheduler und 8 im Tunnel-Manager.

Dass das Limit real trifft, ist im Code dokumentiert — websocket/manager.py:95:

Sonst blieb eine Pool-Connection samt Row-Locks für die gesamte Command-Dauer belegt ("idle in transaction") → bei langsamen/hängenden Agenten Pileup → QueuePool-Exhaustion (500er im ganzen Backend).

Das wurde damals am Symptom behoben (Session pro Log-Schreibvorgang statt Aufrufer-Session), nicht an der Pool-Dimensionierung. Bei Pool-Erschöpfung liefert das ganze Backend 500er, nicht nur der betroffene Pfad.

Zusätzlich fehlt

  • pool_recycle — bei einer Firewall oder einem PgBouncer zwischen Backend und DB werden lange offene Verbindungen stillschweigend gekappt; pool_pre_ping erkennt das erst beim Zugriff
  • pool_timeout — Default 30 s; bei Erschöpfung hängen Requests eine halbe Minute, statt schnell zu scheitern
  • Keine Sichtbarkeit: bei einem 500er ist nicht erkennbar, ob der Pool die Ursache war

Prompt für Claude Code

Dimensioniere den DB-Connection-Pool in theProx für Mehr-Node-Betrieb und mache
ihn beobachtbar.

--- 1. POOL KONFIGURIEREN ---
server/database.py: create_async_engine mit expliziten, über Umgebung
überschreibbaren Werten:

    DB_POOL_SIZE     = int(os.getenv("DB_POOL_SIZE", "20"))
    DB_MAX_OVERFLOW  = int(os.getenv("DB_MAX_OVERFLOW", "30"))
    DB_POOL_TIMEOUT  = int(os.getenv("DB_POOL_TIMEOUT", "10"))
    DB_POOL_RECYCLE  = int(os.getenv("DB_POOL_RECYCLE", "1800"))

    engine = create_async_engine(
        DATABASE_URL, echo=False, pool_pre_ping=True,
        pool_size=DB_POOL_SIZE, max_overflow=DB_MAX_OVERFLOW,
        pool_timeout=DB_POOL_TIMEOUT, pool_recycle=DB_POOL_RECYCLE,
    )

pool_timeout bewusst KLEINER als der Default (10 statt 30 s): bei Erschöpfung
ist schnelles Scheitern mit klarer Meldung besser als ein halbminütiger Hänger,
der sich als Timeout am Frontend zeigt.

--- 2. POSTGRES-SEITE PRÜFEN ---
20+30 = 50 Verbindungen vom Backend. Postgres-Default max_connections ist 100.
docker-compose.yml prüfen: ist dort ein max_connections gesetzt, muss es zu
DB_POOL_SIZE+DB_MAX_OVERFLOW passen und Luft für Wartung/psql lassen. Wenn
nicht gesetzt, in der .env.example dokumentieren, dass beide Werte
zusammenhängen. Bei Änderung von max_connections steigt der
shared_buffers-Bedarf — im Kommentar erwähnen, nicht selbst ändern.

--- 3. BEOBACHTBARKEIT ---
Neuer Endpunkt GET /api/admin/db-pool (require_superadmin):

    {"size": n, "checked_in": n, "checked_out": n, "overflow": n,
     "total": n, "configured": {...}}

Werte aus engine.pool (.size(), .checkedin(), .checkedout(), .overflow()).
Zusätzlich beim Start eine Log-Zeile mit der effektiven Konfiguration.

--- 4. FRÜHWARNUNG ---
Im bestehenden Health-/Monitoring-Pfad: wenn checked_out dauerhaft
> 80 % von (pool_size + max_overflow), eine WARN-Zeile mit den Zahlen. Nicht
als Notification — das ist ein Betriebswert, kein Alarm. Ohne diese Zeile ist
Pool-Erschöpfung im Nachhinein nicht von anderen 500ern zu unterscheiden.

--- 5. SESSION-CHURN IM AGENT-HANDLER PRÜFEN ---
websocket/agent_ws.py öffnet an 12 Stellen AsyncSessionLocal(). Prüfen, ob
innerhalb EINER Nachrichtenverarbeitung mehrere Sessions nacheinander geöffnet
werden, die sich zu einer zusammenfassen lassen. Wo das ohne Verlängerung der
Transaktionsdauer möglich ist: zusammenfassen. Wo eine kurze eigene Session
absichtlich gewählt wurde (siehe manager.py:95), UNVERÄNDERT lassen und den
Grund als Kommentar festhalten — diese Trennung war ein bewusster Fix.

Keine spekulative Umstellung auf eine Session pro WS-Verbindung: das würde
Transaktionen über die gesamte Verbindungsdauer offenhalten und genau das
Problem zurückbringen, das behoben wurde.

--- 6. TEST ---
tests/test_db_pool.py: Konfiguration wird aus der Umgebung übernommen;
der Pool-Endpunkt liefert plausible Werte; require_superadmin greift.

Definition of Done

  • Pool explizit konfiguriert und per Umgebung anpassbar
  • pool_recycle gesetzt
  • Verhältnis zu Postgres max_connections dokumentiert
  • Pool-Zustand über Endpunkt sichtbar
  • Warnung bei hoher Auslastung im Log
  • Effektive Konfiguration beim Start geloggt
**Blockiert das Anbinden weiterer Produktiv-Nodes.** ## Befund `server/database.py:15`: ```python engine = create_async_engine(DATABASE_URL, echo=False, pool_pre_ping=True) ``` Weder `pool_size` noch `max_overflow` gesetzt. SQLAlchemy-Defaults: `pool_size=5`, `max_overflow=10` → **maximal 15 gleichzeitige Verbindungen** für das gesamte Backend. ## Auswirkung Der Verbrauch skaliert mit der Node-Zahl. `websocket/agent_ws.py` öffnet an **12 Stellen** eine eigene `AsyncSessionLocal()`, überwiegend im Nachrichten-Handler — also pro eingehender Agent-Nachricht. Bei `scan_interval` von 300 s (Default, `models/node.py:53`) plus Heartbeats, Log-Batches, Task-Updates und Tunnel-Events erzeugt jeder Node dauerhaft Sessions. Dazu die Request-Sessions des Frontends und je 9 Stellen im Scheduler und 8 im Tunnel-Manager. Dass das Limit real trifft, ist im Code dokumentiert — `websocket/manager.py:95`: > Sonst blieb eine Pool-Connection samt Row-Locks für die gesamte Command-Dauer belegt ("idle in transaction") → bei langsamen/hängenden Agenten Pileup → QueuePool-Exhaustion (500er im ganzen Backend). Das wurde damals am Symptom behoben (Session pro Log-Schreibvorgang statt Aufrufer-Session), nicht an der Pool-Dimensionierung. Bei Pool-Erschöpfung liefert **das ganze Backend** 500er, nicht nur der betroffene Pfad. ## Zusätzlich fehlt - `pool_recycle` — bei einer Firewall oder einem PgBouncer zwischen Backend und DB werden lange offene Verbindungen stillschweigend gekappt; `pool_pre_ping` erkennt das erst beim Zugriff - `pool_timeout` — Default 30 s; bei Erschöpfung hängen Requests eine halbe Minute, statt schnell zu scheitern - Keine Sichtbarkeit: bei einem 500er ist nicht erkennbar, ob der Pool die Ursache war --- ## Prompt für Claude Code ``` Dimensioniere den DB-Connection-Pool in theProx für Mehr-Node-Betrieb und mache ihn beobachtbar. --- 1. POOL KONFIGURIEREN --- server/database.py: create_async_engine mit expliziten, über Umgebung überschreibbaren Werten: DB_POOL_SIZE = int(os.getenv("DB_POOL_SIZE", "20")) DB_MAX_OVERFLOW = int(os.getenv("DB_MAX_OVERFLOW", "30")) DB_POOL_TIMEOUT = int(os.getenv("DB_POOL_TIMEOUT", "10")) DB_POOL_RECYCLE = int(os.getenv("DB_POOL_RECYCLE", "1800")) engine = create_async_engine( DATABASE_URL, echo=False, pool_pre_ping=True, pool_size=DB_POOL_SIZE, max_overflow=DB_MAX_OVERFLOW, pool_timeout=DB_POOL_TIMEOUT, pool_recycle=DB_POOL_RECYCLE, ) pool_timeout bewusst KLEINER als der Default (10 statt 30 s): bei Erschöpfung ist schnelles Scheitern mit klarer Meldung besser als ein halbminütiger Hänger, der sich als Timeout am Frontend zeigt. --- 2. POSTGRES-SEITE PRÜFEN --- 20+30 = 50 Verbindungen vom Backend. Postgres-Default max_connections ist 100. docker-compose.yml prüfen: ist dort ein max_connections gesetzt, muss es zu DB_POOL_SIZE+DB_MAX_OVERFLOW passen und Luft für Wartung/psql lassen. Wenn nicht gesetzt, in der .env.example dokumentieren, dass beide Werte zusammenhängen. Bei Änderung von max_connections steigt der shared_buffers-Bedarf — im Kommentar erwähnen, nicht selbst ändern. --- 3. BEOBACHTBARKEIT --- Neuer Endpunkt GET /api/admin/db-pool (require_superadmin): {"size": n, "checked_in": n, "checked_out": n, "overflow": n, "total": n, "configured": {...}} Werte aus engine.pool (.size(), .checkedin(), .checkedout(), .overflow()). Zusätzlich beim Start eine Log-Zeile mit der effektiven Konfiguration. --- 4. FRÜHWARNUNG --- Im bestehenden Health-/Monitoring-Pfad: wenn checked_out dauerhaft > 80 % von (pool_size + max_overflow), eine WARN-Zeile mit den Zahlen. Nicht als Notification — das ist ein Betriebswert, kein Alarm. Ohne diese Zeile ist Pool-Erschöpfung im Nachhinein nicht von anderen 500ern zu unterscheiden. --- 5. SESSION-CHURN IM AGENT-HANDLER PRÜFEN --- websocket/agent_ws.py öffnet an 12 Stellen AsyncSessionLocal(). Prüfen, ob innerhalb EINER Nachrichtenverarbeitung mehrere Sessions nacheinander geöffnet werden, die sich zu einer zusammenfassen lassen. Wo das ohne Verlängerung der Transaktionsdauer möglich ist: zusammenfassen. Wo eine kurze eigene Session absichtlich gewählt wurde (siehe manager.py:95), UNVERÄNDERT lassen und den Grund als Kommentar festhalten — diese Trennung war ein bewusster Fix. Keine spekulative Umstellung auf eine Session pro WS-Verbindung: das würde Transaktionen über die gesamte Verbindungsdauer offenhalten und genau das Problem zurückbringen, das behoben wurde. --- 6. TEST --- tests/test_db_pool.py: Konfiguration wird aus der Umgebung übernommen; der Pool-Endpunkt liefert plausible Werte; require_superadmin greift. ``` ## Definition of Done - [ ] Pool explizit konfiguriert und per Umgebung anpassbar - [ ] `pool_recycle` gesetzt - [ ] Verhältnis zu Postgres `max_connections` dokumentiert - [ ] Pool-Zustand über Endpunkt sichtbar - [ ] Warnung bei hoher Auslastung im Log - [ ] Effektive Konfiguration beim Start geloggt
Author
Owner

Aus dem Code-Review vom 2026-07-27 (Commit 4152c4d). Gesamtblock: #226, #227, #228, #229, #230, #231.

Abarbeitung niedrig → hoch: #231#230#229#228#227#226.

Aus dem Code-Review vom 2026-07-27 (Commit 4152c4d). Gesamtblock: #226, #227, #228, #229, #230, #231. Abarbeitung niedrig → hoch: #231 → #230 → #229 → #228 → #227 → #226.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
chinux/theProx#227
No description provided.