# Beteiligte Sync - Event Handler Event-driven sync handler für bidirektionale Synchronisation von Beteiligten (Stammdaten). ## Subscribes - `vmh.beteiligte.create` - Neuer Beteiligter in EspoCRM - `vmh.beteiligte.update` - Änderung in EspoCRM - `vmh.beteiligte.delete` - Löschung in EspoCRM - `vmh.beteiligte.sync_check` - Cron-triggered check ## Funktionsweise ### 1. Event empfangen ```json { "entity_id": "68e3e7eab49f09adb", "action": "sync_check", "source": "cron", "timestamp": "2026-02-07T16:00:00" } ``` ### 2. Lock acquisition (Redis) ```python lock_key = f"sync_lock:cbeteiligte:{entity_id}" acquired = redis.set(lock_key, "locked", nx=True, ex=300) ``` - **Atomar** via Redis `SET NX` - **TTL**: 5 Minuten (verhindert Deadlocks) - **Verhindert**: Parallele Syncs derselben Entity ### 3. Routing nach Action #### CREATE (kein betnr) ``` Map EspoCRM → Advoware ↓ POST /api/v1/advonet/Beteiligte ↓ Response: {betNr: 12345} ↓ Update EspoCRM: betnr=12345, syncStatus=clean (combined!) ``` #### UPDATE (hat betnr) ``` GET /api/v1/advonet/Beteiligte/{betnr} ↓ Timestamp-Vergleich (modifiedAt vs geaendertAm) ↓ ├─ espocrm_newer → PUT to Advoware ├─ advoware_newer → PATCH to EspoCRM ├─ conflict → EspoCRM wins + Notification └─ no_change → Skip ``` ### 4. Lock release ```python await sync_utils.release_sync_lock( entity_id, 'clean', extra_fields={'betnr': new_betnr} # Optional: combine operations ) ``` - Updates `syncStatus`, `advowareLastSync`, `syncRetryCount` - Optional: Merge zusätzliche Felder (betnr, etc.) - Löscht Redis lock ## Optimierungen ### Redis Distributed Lock ```python # VORHER: Nicht-atomar (Race Condition möglich) entity = await get_entity(...) if entity.syncStatus == 'syncing': return await update_entity(..., {'syncStatus': 'syncing'}) # NACHHER: Atomarer Redis lock acquired = redis.set(lock_key, "locked", nx=True, ex=300) if not acquired: return ``` ### Combined API Calls ```python # VORHER: 2 API calls await release_sync_lock(entity_id, 'clean') await update_entity(entity_id, {'betnr': new_betnr}) # NACHHER: 1 API call (33% faster) await release_sync_lock( entity_id, 'clean', extra_fields={'betnr': new_betnr} ) ``` ### Merge Utility ```python # Keine Code-Duplikation mehr (3x → 1x) merged_data = sync_utils.merge_for_advoware_put( advo_entity, espo_entity, mapper ) ``` ## Error Handling ### Retriable Errors - Netzwerk-Timeout → `syncStatus=failed`, retry beim nächsten Cron - 500 Server Error → `syncStatus=failed`, retry - Redis unavailable → Fallback zu syncStatus-only lock ### Non-Retriable Errors - 400 Bad Request → `syncStatus=failed`, keine Auto-Retry - 404 Not Found → Entity gelöscht, markiere als `deleted_in_advoware` - 401 Auth Error → `syncStatus=failed`, keine Auto-Retry ### Max Retries ```python if retry_count >= 5: syncStatus = 'permanently_failed' send_notification("Max retries exceeded") ``` ## Performance | Metric | Value | |--------|-------| | Latency (CREATE) | ~200ms | | Latency (UPDATE) | ~250ms | | API Calls (CREATE) | 2 | | API Calls (UPDATE) | 2 | | Lock Timeout | 5 min | ## Dependencies - [services/espocrm.py](../../services/espocrm.py) - EspoCRM API - [services/advoware.py](../../services/advoware.py) - Advoware API - [services/espocrm_mapper.py](../../services/espocrm_mapper.py) - Entity mapper - [services/beteiligte_sync_utils.py](../../services/beteiligte_sync_utils.py) - Sync utilities - Redis (localhost:6379, DB 1) - Distributed locking ## Testing ```python # Test event event_data = { 'entity_id': '68e3e7eab49f09adb', 'action': 'sync_check', 'source': 'test' } await handler(event_data, context) # Verify entity = await espocrm.get_entity('CBeteiligte', entity_id) assert entity['syncStatus'] == 'clean' assert entity['betnr'] is not None ``` ## Siehe auch - [Beteiligte Sync Docs](../../docs/BETEILIGTE_SYNC.md) - Vollständige Dokumentation - [Cron Step](beteiligte_sync_cron_step.py) - Findet Entities für Sync - [Sync Utils](../../services/beteiligte_sync_utils.py) - Helper functions