import json import signal from contextlib import AbstractContextManager from functools import wraps from redis.lock import Lock as RedisLock from apollo_notifications.config import LockConfig class ControlledLock(AbstractContextManager): """Context manager wrapper for redis lock with extra 'enabled' parameter, non-blocking check and releasing on SIGTERM.""" lock: RedisLock = None def __init__(self, redis_client, key: str, ttl: int, enabled: bool): self.redis_client = redis_client self.lock_key = key self.lock_ttl = ttl self.lock_enabled = enabled self.locked = False if enabled: self.lock = self.redis_client.lock(self.lock_key, timeout=self.lock_ttl) # for deployment: we should release distributed lock on service shutting down signal.signal(signal.SIGTERM, self._sigterm_handler) def __enter__(self): if not self.lock or not self.lock.acquire(blocking=False): return self.locked = True return self.lock def __exit__(self, exc_type, exc_val, exc_tb): self._release() def _sigterm_handler(self, signum, frame): self._release() def _release(self): if self.locked: self.lock.release() def lock(logger, config: LockConfig, redis_client, sentry=None): """Decorator for locks managing and exceptions handling.""" def decorator(f): @wraps(f) def wrapper(*args, **kwargs): with ControlledLock( redis_client, f"{config.APP_NAME}/lock", config.REDIS_LOCK_TTL, config.REDIS_LOCK_ENABLED) as lock: try: if config.REDIS_LOCK_ENABLED and not lock: logger.info(f"{config.APP_NAME}: can not get lock.") return return f(*args, **kwargs) except Exception as ex: sentry and sentry.capture_exception(ex) raise return wrapper return decorator class ConstantKeyCache: """Decorator class for caching with constant key.""" def __init__(self, redis_client, key: str, ttl: int): self.redis_client = redis_client self.ttl = ttl self.key = key def __call__(self, f): @wraps(f) def wrapper(*args, **kwargs): cached_value = self.redis_client.get(self.key) if cached_value: return json.loads(cached_value) result = f(*args, **kwargs) self.redis_client.setex(self.key, self.ttl, json.dumps(result, default=str)) return result return wrapper def cache(redis_client, key: str, ttl: int): """Simple cache decorator.""" def decorator(f): @wraps(f) def wrapper(*args, **kwargs): cached_value = redis_client.get(key) if cached_value: return json.loads(cached_value) result = f(*args, **kwargs) redis_client.setex(key, ttl, json.dumps(result, default=str)) return result return wrapper return decorator