Odoo Poller Robustheit: Race Conditions, Datenverlust und Performance #74

Open
opened 2026-03-30 20:19:10 +00:00 by David · 1 comment
Collaborator

Beschreibung

Der Odoo Poller hat mehrere Robustheitsprobleme: Race Conditions bei der
Duplicate Detection, Datenverlust wenn Tag-Resolution fehlschlägt,
Performance-Probleme durch fehlendes inkrementelles Polling und
unkontrollierte Image-Downloads.

Hintergrund

Bei hohem Ticket-Aufkommen oder instabiler Odoo-Verbindung können Tickets
verloren gehen, doppelt angelegt werden oder der gesamte Polling-Zyklus
blockiert werden. Diese Probleme verschärfen sich mit Multi-Tenant (#63).

Findings

1. Race Condition bei Duplicate Detection (CRITICAL)

Datei: backend/services/odoo_poller.py Zeile 364-394

existing = db.query(Ticket).filter(Ticket.odoo_id == odoo_id).first()
if existing:
    # update
else:
    # create  ← Zwischen Check und Insert kann ein anderer Poll dasselbe Ticket anlegen
  • Check-then-insert ohne DB-Lock → IntegrityError bei parallelen Polls
  • Fix: PostgreSQL INSERT ... ON CONFLICT DO UPDATE (upsert) verwenden

2. Tag-Resolution bricht gesamten Poll ab (CRITICAL)

Datei: backend/services/odoo_poller.py Zeile 305-328

  • Wenn _resolve_tag_names() fehlschlägt (Odoo unter Last), werden ALLE
    Tickets des Zyklus verworfen
  • Fix: Tag-Resolution non-fatal machen — Tickets ohne Tags anlegen statt alles abbrechen

3. Image-Download ohne Concurrency-Limit (MEDIUM)

Datei: backend/services/odoo_poller.py Zeile 161-215

  • 50 Bilder × 15s Timeout = 750s Blocking
  • Kein Limit auf Anzahl oder Parallelität
  • Fix: asyncio.Semaphore(3) + max_images=10 Limit

4. httpx.AsyncClient wird nie geschlossen (MEDIUM)

Datei: backend/services/odoo_poller.py Zeile 284-290

  • OdooClient._client wird bei App-Start erstellt, close() nie aufgerufen
  • Connection-Pool wächst unbegrenzt
  • Fix: In FastAPI Lifespan-Handler await poller.close() aufrufen

5. Kein inkrementelles Polling (MEDIUM)

Datei: backend/services/odoo_poller.py

  • Jeder Poll holt ALLE Tickets die zum Filter passen, statt nur geänderte
  • Bei 1000+ Tickets in Odoo: unnötige API-Last
  • Fix: ["write_date", ">=", last_poll_time] zum Domain-Filter hinzufügen

6. Kein Poll-Lock bei Überlappung (MEDIUM)

Datei: backend/main.py (Scheduler)

  • Wenn ein Poll >5min dauert, startet der nächste parallel
  • Beide holen dieselben Tickets → Finding #1 wird getriggert
  • Fix: asyncio.Lock() im Scheduler, Skip wenn bereits laufend

7. Weitere kleinere Findings

  • Image-Overwrite bei partiellem Extraktions-Fehler (Zeile 372)
  • Keine Validierung der Odoo-Response-Struktur (Zeile 335)
  • Silent Failures in Image-Extraction ohne Error-Logging (Zeile 210-211)
  • Inkonsistente MIME-Type Whitelist (Zeile 226 vs 248)
  • Hardcoded IMAGES_DIR nicht konfigurierbar (Zeile 18)

Akzeptanzkriterien

  • Ticket-Upsert via PostgreSQL ON CONFLICT DO UPDATE statt check-then-insert
  • Tag-Resolution-Fehler ist non-fatal — Tickets werden ohne Tags angelegt
  • Image-Downloads mit Semaphore (max 3 parallel) und max 10 Bilder pro Ticket
  • httpx.AsyncClient wird im Lifespan-Handler geschlossen
  • Inkrementelles Polling via write_date >= last_poll_time
  • Poll-Lock verhindert überlappende Polling-Zyklen
  • Tests: Upsert bei doppeltem odoo_id
  • Tests: Poll mit fehlgeschlagener Tag-Resolution liefert trotzdem Tickets
  • Tests: Image-Download Timeout und Limit
  • Tests: Concurrent Poll wird übersprungen

Technische Hinweise

  • Betroffene Dateien:
    • backend/services/odoo_poller.py (Hauptänderungen: upsert, tag-fallback, image-limits)
    • backend/main.py (Poll-Lock, Lifespan close)
    • backend/config.py (ggf. images_dir, max_images_per_ticket Settings)
  • Ansatz: Schrittweise — erst upsert + tag-fallback (kritisch), dann Performance
  • Migration nötig: Nein

Aufwand: M

## Beschreibung Der Odoo Poller hat mehrere Robustheitsprobleme: Race Conditions bei der Duplicate Detection, Datenverlust wenn Tag-Resolution fehlschlägt, Performance-Probleme durch fehlendes inkrementelles Polling und unkontrollierte Image-Downloads. ## Hintergrund Bei hohem Ticket-Aufkommen oder instabiler Odoo-Verbindung können Tickets verloren gehen, doppelt angelegt werden oder der gesamte Polling-Zyklus blockiert werden. Diese Probleme verschärfen sich mit Multi-Tenant (#63). ## Findings ### 1. Race Condition bei Duplicate Detection (CRITICAL) **Datei:** `backend/services/odoo_poller.py` Zeile 364-394 ```python existing = db.query(Ticket).filter(Ticket.odoo_id == odoo_id).first() if existing: # update else: # create ← Zwischen Check und Insert kann ein anderer Poll dasselbe Ticket anlegen ``` - Check-then-insert ohne DB-Lock → IntegrityError bei parallelen Polls - **Fix:** PostgreSQL `INSERT ... ON CONFLICT DO UPDATE` (upsert) verwenden ### 2. Tag-Resolution bricht gesamten Poll ab (CRITICAL) **Datei:** `backend/services/odoo_poller.py` Zeile 305-328 - Wenn `_resolve_tag_names()` fehlschlägt (Odoo unter Last), werden ALLE Tickets des Zyklus verworfen - **Fix:** Tag-Resolution non-fatal machen — Tickets ohne Tags anlegen statt alles abbrechen ### 3. Image-Download ohne Concurrency-Limit (MEDIUM) **Datei:** `backend/services/odoo_poller.py` Zeile 161-215 - 50 Bilder × 15s Timeout = 750s Blocking - Kein Limit auf Anzahl oder Parallelität - **Fix:** `asyncio.Semaphore(3)` + `max_images=10` Limit ### 4. httpx.AsyncClient wird nie geschlossen (MEDIUM) **Datei:** `backend/services/odoo_poller.py` Zeile 284-290 - `OdooClient._client` wird bei App-Start erstellt, `close()` nie aufgerufen - Connection-Pool wächst unbegrenzt - **Fix:** In FastAPI Lifespan-Handler `await poller.close()` aufrufen ### 5. Kein inkrementelles Polling (MEDIUM) **Datei:** `backend/services/odoo_poller.py` - Jeder Poll holt ALLE Tickets die zum Filter passen, statt nur geänderte - Bei 1000+ Tickets in Odoo: unnötige API-Last - **Fix:** `["write_date", ">=", last_poll_time]` zum Domain-Filter hinzufügen ### 6. Kein Poll-Lock bei Überlappung (MEDIUM) **Datei:** `backend/main.py` (Scheduler) - Wenn ein Poll >5min dauert, startet der nächste parallel - Beide holen dieselben Tickets → Finding #1 wird getriggert - **Fix:** `asyncio.Lock()` im Scheduler, Skip wenn bereits laufend ### 7. Weitere kleinere Findings - Image-Overwrite bei partiellem Extraktions-Fehler (Zeile 372) - Keine Validierung der Odoo-Response-Struktur (Zeile 335) - Silent Failures in Image-Extraction ohne Error-Logging (Zeile 210-211) - Inkonsistente MIME-Type Whitelist (Zeile 226 vs 248) - Hardcoded `IMAGES_DIR` nicht konfigurierbar (Zeile 18) ## Akzeptanzkriterien - [ ] Ticket-Upsert via PostgreSQL `ON CONFLICT DO UPDATE` statt check-then-insert - [ ] Tag-Resolution-Fehler ist non-fatal — Tickets werden ohne Tags angelegt - [ ] Image-Downloads mit Semaphore (max 3 parallel) und max 10 Bilder pro Ticket - [ ] httpx.AsyncClient wird im Lifespan-Handler geschlossen - [ ] Inkrementelles Polling via `write_date >= last_poll_time` - [ ] Poll-Lock verhindert überlappende Polling-Zyklen - [ ] Tests: Upsert bei doppeltem `odoo_id` - [ ] Tests: Poll mit fehlgeschlagener Tag-Resolution liefert trotzdem Tickets - [ ] Tests: Image-Download Timeout und Limit - [ ] Tests: Concurrent Poll wird übersprungen ## Technische Hinweise - Betroffene Dateien: - `backend/services/odoo_poller.py` (Hauptänderungen: upsert, tag-fallback, image-limits) - `backend/main.py` (Poll-Lock, Lifespan close) - `backend/config.py` (ggf. `images_dir`, `max_images_per_ticket` Settings) - Ansatz: Schrittweise — erst upsert + tag-fallback (kritisch), dann Performance - Migration nötig: Nein ## Aufwand: M
Author
Collaborator

Hinweis: #44 (Stuck Tickets Cleanup) und #77 (Fehlerbehandlung Pipeline) wurden in dieses Issue konsolidiert. Alle relevanten Findings sind hier abgedeckt.

Hinweis: #44 (Stuck Tickets Cleanup) und #77 (Fehlerbehandlung Pipeline) wurden in dieses Issue konsolidiert. Alle relevanten Findings sind hier abgedeckt.
Sign in to join this conversation.
No description provided.