fix: add try/finally safety net for lock release in beteiligte_sync handler – guarantees Redis key cleanup on any exit path

This commit is contained in:
bsiggel
2026-03-31 07:00:35 +00:00
parent 814e869ca0
commit 3ade3fc3cc

View File

@@ -88,7 +88,7 @@ async def handler(event_data: Dict[str, Any], ctx: FlowContext[Any]) -> None:
ctx.logger.warn(f"⏸️ Sync bereits aktiv für {entity_id}, überspringe") ctx.logger.warn(f"⏸️ Sync bereits aktiv für {entity_id}, überspringe")
return return
# Lock erfolgreich acquired - MUSS im finally block released werden! # Lock erfolgreich acquired
try: try:
# 2. FETCH ENTITY VON ESPOCRM # 2. FETCH ENTITY VON ESPOCRM
try: try:
@@ -97,23 +97,23 @@ async def handler(event_data: Dict[str, Any], ctx: FlowContext[Any]) -> None:
ctx.logger.error(f"❌ Fehler beim Laden von EspoCRM Entity: {e}") ctx.logger.error(f"❌ Fehler beim Laden von EspoCRM Entity: {e}")
await sync_utils.release_sync_lock(entity_id, 'failed', str(e), increment_retry=True) await sync_utils.release_sync_lock(entity_id, 'failed', str(e), increment_retry=True)
return return
ctx.logger.info(f"📋 Entity geladen: {espo_entity.get('name')} (betnr: {espo_entity.get('betnr')})") ctx.logger.info(f"📋 Entity geladen: {espo_entity.get('name')} (betnr: {espo_entity.get('betnr')})")
betnr = espo_entity.get('betnr') betnr = espo_entity.get('betnr')
sync_status = espo_entity.get('syncStatus', 'pending_sync') sync_status = espo_entity.get('syncStatus', 'pending_sync')
# Check Retry-Backoff - überspringe wenn syncNextRetry noch nicht erreicht # Check Retry-Backoff - überspringe wenn syncNextRetry noch nicht erreicht
sync_next_retry = espo_entity.get('syncNextRetry') sync_next_retry = espo_entity.get('syncNextRetry')
if sync_next_retry and sync_status == 'failed': if sync_next_retry and sync_status == 'failed':
import datetime import datetime
import pytz import pytz
try: try:
next_retry_ts = datetime.datetime.strptime(sync_next_retry, '%Y-%m-%d %H:%M:%S') next_retry_ts = datetime.datetime.strptime(sync_next_retry, '%Y-%m-%d %H:%M:%S')
next_retry_ts = pytz.UTC.localize(next_retry_ts) next_retry_ts = pytz.UTC.localize(next_retry_ts)
now_utc = datetime.datetime.now(pytz.UTC) now_utc = datetime.datetime.now(pytz.UTC)
if now_utc < next_retry_ts: if now_utc < next_retry_ts:
remaining_minutes = int((next_retry_ts - now_utc).total_seconds() / 60) remaining_minutes = int((next_retry_ts - now_utc).total_seconds() / 60)
ctx.logger.info(f"⏸️ Retry-Backoff aktiv: Nächster Versuch in {remaining_minutes} Minuten") ctx.logger.info(f"⏸️ Retry-Backoff aktiv: Nächster Versuch in {remaining_minutes} Minuten")
@@ -121,34 +121,34 @@ async def handler(event_data: Dict[str, Any], ctx: FlowContext[Any]) -> None:
return return
except Exception as e: except Exception as e:
ctx.logger.warn(f"⚠️ Fehler beim Parsen von syncNextRetry: {e}") ctx.logger.warn(f"⚠️ Fehler beim Parsen von syncNextRetry: {e}")
# 3. BESTIMME SYNC-AKTION # 3. BESTIMME SYNC-AKTION
# FALL A: Neu (kein betnr) → CREATE in Advoware # FALL A: Neu (kein betnr) → CREATE in Advoware
if not betnr and action in ['create', 'sync_check']: if not betnr and action in ['create', 'sync_check']:
ctx.logger.info(f"🆕 Neuer Beteiligter → CREATE in Advoware") ctx.logger.info(f"🆕 Neuer Beteiligter → CREATE in Advoware")
await handle_create(entity_id, espo_entity, espocrm, advoware, sync_utils, mapper, ctx) await handle_create(entity_id, espo_entity, espocrm, advoware, sync_utils, mapper, ctx)
# FALL B: Existiert (hat betnr) → UPDATE oder CHECK # FALL B: Existiert (hat betnr) → UPDATE oder CHECK
elif betnr: elif betnr:
ctx.logger.info(f"♻️ Existierender Beteiligter (betNr: {betnr}) → UPDATE/CHECK") ctx.logger.info(f"♻️ Existierender Beteiligter (betNr: {betnr}) → UPDATE/CHECK")
await handle_update(entity_id, betnr, espo_entity, espocrm, advoware, sync_utils, mapper, ctx) await handle_update(entity_id, betnr, espo_entity, espocrm, advoware, sync_utils, mapper, ctx)
# FALL C: DELETE (TODO: Implementierung später) # FALL C: DELETE (TODO: Implementierung später)
elif action == 'delete': elif action == 'delete':
ctx.logger.warn(f"🗑️ DELETE noch nicht implementiert für {entity_id}") ctx.logger.warn(f"🗑️ DELETE noch nicht implementiert für {entity_id}")
await sync_utils.release_sync_lock(entity_id, 'failed', 'Delete-Operation nicht implementiert') await sync_utils.release_sync_lock(entity_id, 'failed', 'Delete-Operation nicht implementiert')
else: else:
ctx.logger.warn(f"⚠️ Unbekannte Kombination: action={action}, betnr={betnr}") ctx.logger.warn(f"⚠️ Unbekannte Kombination: action={action}, betnr={betnr}")
await sync_utils.release_sync_lock(entity_id, 'failed', f'Unbekannte Aktion: {action}') await sync_utils.release_sync_lock(entity_id, 'failed', f'Unbekannte Aktion: {action}')
except Exception as e: except Exception as e:
# Unerwarteter Fehler während Sync - GARANTIERE Lock-Release # Unerwarteter Fehler während Sync - Lock-Release via finally
ctx.logger.error(f"❌ Unerwarteter Fehler im Sync-Handler: {e}") ctx.logger.error(f"❌ Unerwarteter Fehler im Sync-Handler: {e}")
import traceback import traceback
ctx.logger.error(traceback.format_exc()) ctx.logger.error(traceback.format_exc())
try: try:
await sync_utils.release_sync_lock( await sync_utils.release_sync_lock(
entity_id, entity_id,
@@ -157,15 +157,16 @@ async def handler(event_data: Dict[str, Any], ctx: FlowContext[Any]) -> None:
increment_retry=True increment_retry=True
) )
except Exception as release_error: except Exception as release_error:
# Selbst Lock-Release failed - logge kritischen Fehler ctx.logger.critical(f"🚨 CRITICAL: release_sync_lock failed für {entity_id}: {release_error}")
ctx.logger.critical(f"🚨 CRITICAL: Lock-Release failed für {entity_id}: {release_error}") raise
# Force Redis lock release
try: finally:
lock_key = f"sync_lock:cbeteiligte:{entity_id}" # Safety net: force Redis lock release regardless of outcome
redis_client.delete(lock_key) try:
ctx.logger.info(f"✅ Redis lock manuell released: {lock_key}") force_lock_key = f"sync_lock:cbeteiligte:{entity_id}"
except: redis_client.delete(force_lock_key)
pass except Exception as cleanup_err:
ctx.logger.error(f"❌ Force Lock-Release fehlgeschlagen: {cleanup_err}")
except Exception as e: except Exception as e:
# Fehler VOR Lock-Acquire - kein Lock-Release nötig # Fehler VOR Lock-Acquire - kein Lock-Release nötig