- Introduced SYNC_STRATEGY_ARCHIVE.md detailing the sync process, status values, and flow for updating entities from EspoCRM to Advoware and vice versa. - Created SYNC_TEMPLATE.md as a guide for implementing new syncs, including field definitions, mapper examples, sync utilities, event handlers, and cron jobs. - Added README_SYNC.md for the Beteiligte sync event handler, outlining its functionality, event subscriptions, optimizations, error handling, and performance metrics.
170 lines
4.1 KiB
Markdown
170 lines
4.1 KiB
Markdown
# 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
|