- Fixed initial sync logic to respect actual timestamps, preventing unwanted overwrites. - Introduced exponential backoff for retry logic, with auto-reset for permanently failed entities. - Added validation checks to ensure data consistency during sync processes. - Corrected hash calculation to only include sync-relevant communications. - Resolved issues with empty slots ignoring user inputs and improved conflict handling. - Enhanced handling of Var4 and Var6 entries during sync conflicts. - Documented changes and added new fields required in EspoCRM for improved sync management. Also added a detailed analysis of syncStatus values in EspoCRM CBeteiligte, outlining responsibilities and ensuring robust sync mechanisms.
13 KiB
Analyse: syncStatus Werte in EspoCRM CBeteiligte
Datum: 8. Februar 2026 (Updated)
Design-Philosophie: Defense in Depth (Webhook + Cron Fallback)
Das System verwendet zwei parallele Sync-Trigger:
- Primary Path (Webhook): Echtzeit-Sync bei Änderungen in EspoCRM
- Fallback Path (Cron): 15-Minuten-Check falls Webhook fehlschlägt
Dies garantiert robuste Synchronisation auch bei temporären Webhook-Ausfällen.
Übersicht: Definierte syncStatus-Werte
Basierend auf Code-Analyse wurden folgende Status identifiziert:
| Status | Bedeutung | Gesetzt von | Zweck |
|---|---|---|---|
pending_sync |
Wartet auf ersten Sync | EspoCRM (bei CREATE) | Cron-Fallback wenn Webhook fehlschlägt |
dirty |
Daten geändert, Sync nötig | EspoCRM (bei UPDATE) | Cron-Fallback wenn Webhook fehlschlägt |
syncing |
Sync läuft gerade | Python (acquire_lock) | Lock während Sync |
clean |
Erfolgreich synchronisiert | Python (release_lock) | Sync erfolgreich |
failed |
Sync fehlgeschlagen (< 5 Retries) | Python (bei Fehler) | Retry mit Backoff |
permanently_failed |
Sync fehlgeschlagen (≥ 5 Retries) | Python (max retries) | Auto-Reset nach 24h |
conflict |
Konflikt erkannt (optional) | Python (bei Konflikt) | UI-Visibility für Konflikte |
deleted_in_advoware |
In Advoware gelöscht (404) | Python (bei 404) | Soft-Delete Strategie |
Status-Verantwortlichkeiten
EspoCRM Verantwortung (Frontend/Hooks):
pending_sync- Bei CREATE neuer CBeteiligte Entitydirty- Bei UPDATE existierender CBeteiligte Entity
Python Verantwortung (Sync-Handler):
syncing- Lock während Sync-Prozessclean- Nach erfolgreichem Syncfailed- Bei Sync-Fehlern mit Retrypermanently_failed- Nach zu vielen Retriesconflict- Bei erkannten Konflikten (optional)deleted_in_advoware- Bei 404 von Advoware API
Detaillierte Analyse
✅ Python-Managed Status (funktionieren perfekt)
1. syncing
Wann gesetzt: Bei acquire_sync_lock() (Line 90)
await self.espocrm.update_entity('CBeteiligte', entity_id, {
'syncStatus': 'syncing'
})
Sinnvoll: ✅ Ja - verhindert parallele Syncs, UI-Feedback
2. clean
Wann gesetzt: Bei release_sync_lock() nach erfolgreichem Sync
await sync_utils.release_sync_lock(entity_id, 'clean')
Verwendungen:
- Nach CREATE: Line 223 (beteiligte_sync_event_step.py)
- Nach espocrm_newer Sync: Line 336
- Nach advoware_newer Sync: Line 369
- Nach Konflikt-Auflösung: Line 423 + 643 (beteiligte_sync_utils.py)
Sinnvoll: ✅ Ja - zeigt erfolgreichen Sync an
3. failed
Wann gesetzt: Bei release_sync_lock() mit increment_retry=True
await sync_utils.release_sync_lock(entity_id, 'failed', str(e), increment_retry=True)
Verwendungen:
- CREATE fehlgeschlagen: Line 235
- UPDATE fehlgeschlagen: Line 431
- Validation fehlgeschlagen: Lines 318, 358, 409
- Exception im Handler: Line 139
Sinnvoll: ✅ Ja - ermöglicht Retry-Logik
4. permanently_failed
Wann gesetzt: Nach ≥ 5 Retries (Line 162, beteiligte_sync_utils.py)
if new_retry >= MAX_SYNC_RETRIES:
update_data['syncStatus'] = 'permanently_failed'
Auto-Reset: Nach 24h durch Cron (Lines 64-85, beteiligte_sync_cron_step.py)
{'type': 'equals', 'attribute': 'syncStatus', 'value': 'permanently_failed'}
# → Reset zu 'failed' nach 24h
Sinnvoll: ✅ Ja - verhindert endlose Retries, aber ermöglicht Recovery
5. deleted_in_advoware
Wann gesetzt: Bei 404 von Advoware API (Line 530, beteiligte_sync_utils.py)
await self.espocrm.update_entity('CBeteiligte', entity_id, {
'syncStatus': 'deleted_in_advoware',
'advowareDeletedAt': now,
'syncErrorMessage': f"Beteiligter existiert nicht mehr in Advoware. {error_details}"
})
Sinnvoll: ✅ Ja - Soft-Delete Strategie, ermöglicht manuelle Überprüfung
<EFBFBD> EspoCRM-Managed Status (Webhook-Trigger + Cron-Fallback)
Diese Status werden von EspoCRM gesetzt (nicht vom Python-Code):
6. pending_sync ✅
Wann gesetzt: Von EspoCRM bei CREATE neuer CBeteiligte Entity
Zweck:
- Primary: Webhook
vmh.beteiligte.createtriggert sofort - Fallback: Falls Webhook fehlschlägt, findet Cron diese Entities
Cron-Query (Line 45, beteiligte_sync_cron_step.py):
{'type': 'equals', 'attribute': 'syncStatus', 'value': 'pending_sync'}
Workflow:
1. User erstellt CBeteiligte in EspoCRM
2. EspoCRM setzt syncStatus = 'pending_sync'
3. PRIMARY: EspoCRM Webhook → vmh.beteiligte.create → Sofortiger Sync
FALLBACK: Webhook failed → Cron (alle 15min) findet Entity via Status
4. Python Sync-Handler: pending_sync → syncing → clean/failed
Sinnvoll: ✅ Ja - Defense in Depth Design, garantiert Sync auch bei Webhook-Ausfall
7. dirty ✅
Wann gesetzt: Von EspoCRM bei UPDATE existierender CBeteiligte Entity
Zweck:
- Primary: Webhook
vmh.beteiligte.updatetriggert sofort - Fallback: Falls Webhook fehlschlägt, findet Cron diese Entities
Cron-Query (Line 46, beteiligte_sync_cron_step.py):
{'type': 'equals', 'attribute': 'syncStatus', 'value': 'dirty'}
Workflow:
1. User ändert CBeteiligte in EspoCRM
2. EspoCRM setzt syncStatus = 'dirty' (nur wenn vorher 'clean')
3. PRIMARY: EspoCRM Webhook → vmh.beteiligte.update → Sofortiger Sync
FALLBACK: Webhook failed → Cron (alle 15min) findet Entity via Status
4. Python Sync-Handler: dirty → syncing → clean/failed
Sinnvoll: ✅ Ja - Defense in Depth Design, garantiert Sync auch bei Webhook-Ausfall
Implementation in EspoCRM:
// EspoCRM Hook: afterSave() in CBeteiligte
entity.set('syncStatus', entity.isNew() ? 'pending_sync' : 'dirty');
8. conflict ⚠️ (Optional)
Wann gesetzt: Aktuell NIE - Konflikte werden sofort auto-resolved
Aktuelles Verhalten:
# Bei Konflikt-Erkennung:
if comparison == 'conflict':
# ... löse Konflikt (EspoCRM wins)
await sync_utils.resolve_conflict_espocrm_wins(...)
# Status geht direkt zu 'clean'
Potential für Verbesserung:
# Option: Intermediate 'conflict' Status für Admin-Review
if comparison == 'conflict' and not AUTO_RESOLVE_CONFLICTS:
await espocrm.update_entity('CBeteiligte', entity_id, {
'syncStatus': 'conflict',
'conflictDetails': conflict_details
})
# Warte auf Admin-Aktion
else:
# Auto-Resolve wie aktuell
Status: ⚠️ Optional - Aktuelles Auto-Resolve funktioniert, aber conflict Status könnte UI-Visibility verbessern
Cron-Job Queries Analyse
Datei: steps/vmh/beteiligte_sync_cron_step.py
Query 1: Normale Sync-Kandidaten ✅
{
'type': 'or',
'value': [
{'type': 'equals', 'attribute': 'syncStatus', 'value': 'pending_sync'}, # ✅ Von EspoCRM gesetzt
{'type': 'equals', 'attribute': 'syncStatus', 'value': 'dirty'}, # ✅ Von EspoCRM gesetzt
{'type': 'equals', 'attribute': 'syncStatus', 'value': 'failed'}, # ✅ Von Python gesetzt
]
}
Status: ✅ Funktioniert perfekt als Fallback-Mechanismus
Design-Vorteil:
- Webhook-Ausfall? Cron findet alle
pending_syncunddirtyEntities - Temporäre Fehler? Cron retried alle
failedEntities mit Backoff - Robustes System mit Defense in Depth
Query 2: Auto-Reset für permanently_failed ✅
{'type': 'equals', 'attribute': 'syncStatus', 'value': 'permanently_failed'}
# + syncAutoResetAt < now
Status: ✅ Funktioniert perfekt
Query 3: Periodic Check für clean Entities ✅
{'type': 'equals', 'attribute': 'syncStatus', 'value': 'clean'}
# + advowareLastSync > 24 Stunden alt
Status: ✅ Funktioniert als zusätzliche Sicherheitsebene
EspoCRM Integration Requirements
Damit das System vollständig funktioniert, muss EspoCRM folgende Status setzen:
1. Bei Entity Creation (beforeSave/afterSave Hook)
// EspoCRM: CBeteiligte Entity Hook
entity.set('syncStatus', 'pending_sync');
2. Bei Entity Update (beforeSave Hook)
// EspoCRM: CBeteiligte Entity Hook
if (!entity.isNew() && entity.get('syncStatus') === 'clean') {
// Prüfe ob sync-relevante Felder geändert wurden
const syncRelevantFields = ['name', 'vorname', 'anrede', 'geburtsdatum',
'rechtsform', 'strasse', 'plz', 'ort',
'emailAddressData', 'phoneNumberData'];
const hasChanges = syncRelevantFields.some(field => entity.isAttributeChanged(field));
if (hasChanges) {
entity.set('syncStatus', 'dirty');
}
}
3. Entity Definition (entityDefs/CBeteiligte.json)
{
"fields": {
"syncStatus": {
"type": "enum",
"options": [
"pending_sync",
"dirty",
"syncing",
"clean",
"failed",
"permanently_failed",
"conflict",
"deleted_in_advoware"
],
"default": "pending_sync",
"required": true,
"readOnly": true
}
}
}
System-Architektur: Vollständiger Flow
Szenario 1: CREATE (Happy Path mit Webhook)
1. User erstellt CBeteiligte in EspoCRM
2. EspoCRM Hook setzt syncStatus = 'pending_sync'
3. EspoCRM Webhook triggert vmh.beteiligte.create Event
4. Python Event-Handler:
- acquire_lock() → syncStatus = 'syncing'
- handle_create() → POST zu Advoware
- release_lock() → syncStatus = 'clean'
5. ✅ Erfolgreich synchronisiert
Szenario 2: CREATE (Webhook failed → Cron Fallback)
1. User erstellt CBeteiligte in EspoCRM
2. EspoCRM Hook setzt syncStatus = 'pending_sync'
3. ❌ Webhook Service down/failed
4. 15 Minuten später: Cron läuft
5. Cron Query findet Entity via syncStatus = 'pending_sync'
6. Cron emittiert vmh.beteiligte.sync_check Event
7. Python Event-Handler wie in Szenario 1
8. ✅ Erfolgreich synchronisiert (mit Verzögerung)
Szenario 3: UPDATE (Happy Path mit Webhook)
1. User ändert CBeteiligte in EspoCRM
2. EspoCRM Hook setzt syncStatus = 'dirty' (war vorher 'clean')
3. EspoCRM Webhook triggert vmh.beteiligte.update Event
4. Python Event-Handler:
- acquire_lock() → syncStatus = 'syncing'
- handle_update() → Sync-Logik
- release_lock() → syncStatus = 'clean'
5. ✅ Erfolgreich synchronisiert
Szenario 4: Sync-Fehler mit Retry
1-3. Wie Szenario 1/3
4. Python Event-Handler:
- acquire_lock() → syncStatus = 'syncing'
- handle_xxx() → ❌ Exception
- release_lock(increment_retry=True) → syncStatus = 'failed', syncNextRetry = now + backoff
5. Cron findet Entity via syncStatus = 'failed'
6. Prüft syncNextRetry → noch nicht erreicht → skip
7. Nach Backoff-Zeit: Retry
8. Erfolgreich → syncStatus = 'clean'
ODER nach 5 Retries → syncStatus = 'permanently_failed'
Empfehlungen
✅ Status-Design ist korrekt
Das aktuelle Design mit 8 Status ist optimal für:
- Defense in Depth (Webhook + Cron Fallback)
- Robustheit bei Webhook-Ausfall
- Retry-Mechanismus mit Exponential Backoff
- Soft-Delete Strategie
- UI-Visibility
🔵 EspoCRM Implementation erforderlich
CRITICAL: EspoCRM muss folgende Status setzen:
- ✅
pending_syncbei CREATE - ✅
dirtybei UPDATE (nur wenn vorherclean) - ✅ Default-Wert in Entity Definition
Implementation: EspoCRM Hooks in CBeteiligte Entity
🟡 Optional: Conflict Status
Current: Auto-Resolve funktioniert
Enhancement: Intermediate conflict Status für UI-Visibility und Admin-Review
Zusammenfassung
Status-Verteilung
EspoCRM Verantwortung (2 Status):
- ✅
pending_sync- Bei CREATE - ✅
dirty- Bei UPDATE
Python Verantwortung (6 Status):
- ✅
syncing- Lock während Sync - ✅
clean- Erfolgreich gesynct - ✅
failed- Retry nötig - ✅
permanently_failed- Max retries erreicht - ✅
deleted_in_advoware- 404 von Advoware - ⚠️
conflict- Optional für UI-Visibility
System-Qualität
Architektur: ⭐⭐⭐⭐⭐ (5/5) - Defense in Depth Design Robustheit: ⭐⭐⭐⭐⭐ (5/5) - Funktioniert auch bei Webhook-Ausfall Status-Design: ⭐⭐⭐⭐⭐ (5/5) - Alle Status sinnvoll und notwendig
Einzige Requirement: EspoCRM muss pending_sync und dirty setzen
Review erstellt von: GitHub Copilot
Review-Datum: 8. Februar 2026 (Updated)
Status: ✅ Design validiert, EspoCRM Integration dokumentiert