145 lines
4.7 KiB
Python
145 lines
4.7 KiB
Python
"""
|
|
Base Sync Utilities
|
|
|
|
Gemeinsame Funktionalität für alle Sync-Operationen:
|
|
- Redis Distributed Locking
|
|
- Context-aware Logging
|
|
- EspoCRM API Helpers
|
|
"""
|
|
|
|
from typing import Dict, Any, Optional
|
|
from datetime import datetime
|
|
import pytz
|
|
|
|
from services.exceptions import RedisConnectionError, LockAcquisitionError
|
|
from services.redis_client import get_redis_client
|
|
from services.config import SYNC_CONFIG, get_lock_key
|
|
from services.logging_utils import get_logger
|
|
|
|
import redis
|
|
|
|
|
|
class BaseSyncUtils:
|
|
"""Base-Klasse mit gemeinsamer Sync-Funktionalität"""
|
|
|
|
def __init__(self, espocrm_api, redis_client: Optional[redis.Redis] = None, context=None):
|
|
"""
|
|
Args:
|
|
espocrm_api: EspoCRM API client instance
|
|
redis_client: Optional Redis client (wird sonst über Factory initialisiert)
|
|
context: Optional Motia FlowContext für Logging
|
|
"""
|
|
self.espocrm = espocrm_api
|
|
self.context = context
|
|
self.logger = get_logger('sync_utils', context)
|
|
|
|
# Use provided Redis client or get from factory
|
|
self.redis = redis_client or get_redis_client(strict=False)
|
|
|
|
if not self.redis:
|
|
self.logger.error(
|
|
"⚠️ WARNUNG: Redis nicht verfügbar! "
|
|
"Distributed Locking deaktiviert - Race Conditions möglich!"
|
|
)
|
|
|
|
def _log(self, message: str, level: str = 'info') -> None:
|
|
"""Delegate logging to the logger with optional level"""
|
|
log_func = getattr(self.logger, level, self.logger.info)
|
|
log_func(message)
|
|
|
|
def _get_lock_key(self, entity_id: str) -> str:
|
|
"""
|
|
Erzeugt Redis Lock-Key für eine Entity
|
|
|
|
Muss in Subklassen überschrieben werden, um entity-spezifische Prefixes zu nutzen.
|
|
z.B. 'sync_lock:cbeteiligte:{entity_id}' oder 'sync_lock:document:{entity_id}'
|
|
"""
|
|
raise NotImplementedError("Subclass must implement _get_lock_key()")
|
|
|
|
def _acquire_redis_lock(self, lock_key: str) -> bool:
|
|
"""
|
|
Atomic Redis lock acquisition
|
|
|
|
Args:
|
|
lock_key: Redis key für den Lock
|
|
|
|
Returns:
|
|
True wenn Lock erfolgreich, False wenn bereits locked
|
|
|
|
Raises:
|
|
LockAcquisitionError: Bei kritischen Lock-Problemen (wenn strict mode)
|
|
"""
|
|
if not self.redis:
|
|
self.logger.error(
|
|
"CRITICAL: Distributed Locking deaktiviert - Redis nicht verfügbar!"
|
|
)
|
|
# In production: Dies könnte zu Race Conditions führen!
|
|
# Für jetzt erlauben wir Fortsetzung, aber mit Warning
|
|
return True
|
|
|
|
try:
|
|
acquired = self.redis.set(
|
|
lock_key,
|
|
"locked",
|
|
nx=True,
|
|
ex=SYNC_CONFIG.lock_ttl_seconds
|
|
)
|
|
return bool(acquired)
|
|
except redis.RedisError as e:
|
|
self.logger.error(f"Redis lock error: {e}")
|
|
# Bei Redis-Fehler: Lock erlauben, um Deadlocks zu vermeiden
|
|
return True
|
|
|
|
def _release_redis_lock(self, lock_key: str) -> None:
|
|
"""
|
|
Redis lock freigeben
|
|
|
|
Args:
|
|
lock_key: Redis key für den Lock
|
|
"""
|
|
if not self.redis:
|
|
return
|
|
|
|
try:
|
|
self.redis.delete(lock_key)
|
|
except redis.RedisError as e:
|
|
self.logger.error(f"Redis unlock error: {e}")
|
|
|
|
def _get_espocrm_datetime(self, dt: Optional[datetime] = None) -> str:
|
|
"""
|
|
Formatiert datetime für EspoCRM (ohne Timezone!)
|
|
|
|
Args:
|
|
dt: Optional datetime object (default: now UTC)
|
|
|
|
Returns:
|
|
String im Format 'YYYY-MM-DD HH:MM:SS'
|
|
"""
|
|
if dt is None:
|
|
dt = datetime.now(pytz.UTC)
|
|
elif dt.tzinfo is None:
|
|
dt = pytz.UTC.localize(dt)
|
|
|
|
return dt.strftime('%Y-%m-%d %H:%M:%S')
|
|
|
|
async def acquire_sync_lock(self, entity_id: str, **kwargs) -> bool:
|
|
"""
|
|
Erwirbt Sync-Lock für eine Entity
|
|
|
|
Muss in Subklassen implementiert werden, um entity-spezifische
|
|
Status-Updates durchzuführen.
|
|
|
|
Returns:
|
|
True wenn Lock erfolgreich, False wenn bereits locked
|
|
"""
|
|
raise NotImplementedError("Subclass must implement acquire_sync_lock()")
|
|
|
|
async def release_sync_lock(self, entity_id: str, **kwargs) -> None:
|
|
"""
|
|
Gibt Sync-Lock frei und setzt finalen Status
|
|
|
|
Muss in Subklassen implementiert werden, um entity-spezifische
|
|
Status-Updates durchzuführen.
|
|
"""
|
|
raise NotImplementedError("Subclass must implement release_sync_lock()")
|