172 lines
6.4 KiB
Python
172 lines
6.4 KiB
Python
from __future__ import annotations
|
|
|
|
from .shared import *
|
|
|
|
class ContextCacheInvalidator:
|
|
def __init__(self, redis_url: Optional[str] = None) -> None:
|
|
self.redis_url = redis_url or os.getenv("REDIS_URL")
|
|
self._redis: Optional[Any] = None
|
|
|
|
def _get_redis(self):
|
|
if not REDIS_AVAILABLE or not self.redis_url or RedisSync is None:
|
|
return None
|
|
if self._redis is None:
|
|
self._redis = RedisSync.from_url(
|
|
self.redis_url,
|
|
decode_responses=True,
|
|
socket_connect_timeout=1,
|
|
socket_timeout=1,
|
|
)
|
|
return self._redis
|
|
|
|
@staticmethod
|
|
def _system_key(tenant_id: int) -> str:
|
|
return f"pmflow:context:system:{_safe_int(tenant_id, 1)}"
|
|
|
|
@staticmethod
|
|
def _project_list_key(tenant_id: int) -> str:
|
|
return f"pmflow:project:list:{_safe_int(tenant_id, 1)}"
|
|
|
|
@staticmethod
|
|
def _signal_pending_key(tenant_id: int) -> str:
|
|
return f"pmflow:signal:pending_count:{_safe_int(tenant_id, 1)}"
|
|
|
|
@staticmethod
|
|
def _user_key(tenant_id: int, user_id: int) -> str:
|
|
return f"pmflow:context:user:{_safe_int(tenant_id, 1)}:{_safe_int(user_id, 0)}"
|
|
|
|
def invalidate(
|
|
self,
|
|
*,
|
|
tenant_id: int,
|
|
invalidate_system: bool,
|
|
invalidate_project_list: bool,
|
|
invalidate_signal_pending: bool,
|
|
user_ids: Optional[set[int]] = None,
|
|
) -> None:
|
|
redis = self._get_redis()
|
|
if redis is None:
|
|
return
|
|
|
|
tid = _safe_int(tenant_id, 1)
|
|
keys: list[str] = []
|
|
if invalidate_system:
|
|
keys.append(self._system_key(tid))
|
|
if invalidate_project_list:
|
|
keys.append(self._project_list_key(tid))
|
|
if invalidate_signal_pending:
|
|
keys.append(self._signal_pending_key(tid))
|
|
if user_ids:
|
|
for uid in user_ids:
|
|
if _safe_int(uid, 0) > 0:
|
|
keys.append(self._user_key(tid, uid))
|
|
if not keys:
|
|
return
|
|
try:
|
|
redis.delete(*keys)
|
|
except Exception:
|
|
return
|
|
|
|
def register_context_cache_invalidation_hooks() -> None:
|
|
if getattr(register_context_cache_invalidation_hooks, "_registered", False):
|
|
return
|
|
|
|
invalidator = ContextCacheInvalidator()
|
|
|
|
@event.listens_for(Session, "after_flush")
|
|
def _collect_cache_invalidation(session: Session, _flush_context) -> None: # pragma: no cover
|
|
payload = session.info.get("_pmflow_cache_invalidation")
|
|
if not isinstance(payload, dict):
|
|
payload = {
|
|
"tenant_ids": set(),
|
|
"invalidate_system": False,
|
|
"invalidate_project_list": False,
|
|
"invalidate_signal_pending": False,
|
|
"user_ids": set(),
|
|
}
|
|
session.info["_pmflow_cache_invalidation"] = payload
|
|
|
|
objects = set(session.new).union(session.dirty).union(session.deleted)
|
|
for obj in objects:
|
|
tenant_id = _safe_int(getattr(obj, "tenant_id", 0), 0)
|
|
if tenant_id > 0:
|
|
payload["tenant_ids"].add(tenant_id)
|
|
|
|
if isinstance(obj, Project):
|
|
payload["invalidate_system"] = True
|
|
payload["invalidate_project_list"] = True
|
|
continue
|
|
|
|
if isinstance(obj, ProjectSignalLink):
|
|
payload["invalidate_system"] = True
|
|
payload["invalidate_project_list"] = True
|
|
continue
|
|
|
|
if isinstance(obj, Signal):
|
|
payload["invalidate_system"] = True
|
|
payload["invalidate_signal_pending"] = True
|
|
continue
|
|
|
|
if isinstance(obj, Task):
|
|
payload["invalidate_system"] = True
|
|
state = inspect(obj)
|
|
assignee_hist = state.attrs.assignee_id.history if hasattr(state.attrs, "assignee_id") else None
|
|
if assignee_hist is not None:
|
|
for old_value in assignee_hist.deleted:
|
|
uid = _safe_int(old_value, 0)
|
|
if uid > 0:
|
|
payload["user_ids"].add(uid)
|
|
for new_value in assignee_hist.added:
|
|
uid = _safe_int(new_value, 0)
|
|
if uid > 0:
|
|
payload["user_ids"].add(uid)
|
|
uid = _safe_int(getattr(obj, "assignee_id", 0), 0)
|
|
if uid > 0:
|
|
payload["user_ids"].add(uid)
|
|
continue
|
|
|
|
if isinstance(obj, UserProject):
|
|
payload["invalidate_system"] = True
|
|
payload["invalidate_project_list"] = True
|
|
uid = _safe_int(getattr(obj, "user_id", 0), 0)
|
|
if uid > 0:
|
|
payload["user_ids"].add(uid)
|
|
continue
|
|
|
|
if isinstance(obj, User):
|
|
payload["invalidate_system"] = True
|
|
uid = _safe_int(getattr(obj, "id", 0), 0)
|
|
if uid > 0:
|
|
payload["user_ids"].add(uid)
|
|
continue
|
|
|
|
if isinstance(obj, UserWorkload):
|
|
payload["invalidate_system"] = True
|
|
uid = _safe_int(getattr(obj, "user_id", 0), 0)
|
|
if uid > 0:
|
|
payload["user_ids"].add(uid)
|
|
continue
|
|
|
|
@event.listens_for(Session, "after_commit")
|
|
def _flush_cache_invalidation(session: Session) -> None: # pragma: no cover
|
|
payload = session.info.pop("_pmflow_cache_invalidation", None)
|
|
if not isinstance(payload, dict):
|
|
return
|
|
tenant_ids = payload.get("tenant_ids") or set()
|
|
if not tenant_ids:
|
|
tenant_ids = {_safe_int(os.getenv("DEFAULT_TENANT_ID", "1"), 1)}
|
|
for tenant_id in tenant_ids:
|
|
invalidator.invalidate(
|
|
tenant_id=_safe_int(tenant_id, 1),
|
|
invalidate_system=bool(payload.get("invalidate_system")),
|
|
invalidate_project_list=bool(payload.get("invalidate_project_list")),
|
|
invalidate_signal_pending=bool(payload.get("invalidate_signal_pending")),
|
|
user_ids=set(payload.get("user_ids") or set()),
|
|
)
|
|
|
|
@event.listens_for(Session, "after_rollback")
|
|
def _clear_cache_invalidation(session: Session) -> None: # pragma: no cover
|
|
session.info.pop("_pmflow_cache_invalidation", None)
|
|
|
|
setattr(register_context_cache_invalidation_hooks, "_registered", True)
|