perf(ws): кэш пользователя и доступа к форме на время соединения листа формы
AURORA-1845. process() больше не читает user_service.get / bf_service.get на каждое сообщение: результат кэшируется в ConnectionCache (TTL 30 с, только положительные результаты). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
2a287480b6
commit
1a3d25afce
@ -365,13 +365,71 @@ class ConnectionManager:
|
||||
self.cell_locks.pop(lock_key, None)
|
||||
|
||||
|
||||
# AURORA-1845: срок жизни кэша пользователя/доступа к форме внутри одного
|
||||
# WebSocket-соединения. Ограничивает устаревание (смена роли/орг-юнитов,
|
||||
# удаление формы) при живом соединении; источник истины по правам -
|
||||
# can_edit в SQL-обёртках, он проверяется на каждое сообщение.
|
||||
CONNECTION_CACHE_TTL_SECONDS = 30.0
|
||||
|
||||
|
||||
class ConnectionCache:
|
||||
"""Кэш на время соединения: пользователь и факт доступа к форме.
|
||||
|
||||
Хранится вне процессора (процессор создаётся на каждое сообщение вместе
|
||||
с новой сессией БД). Кэшируются только положительные результаты.
|
||||
"""
|
||||
|
||||
def __init__(self, ttl: float = CONNECTION_CACHE_TTL_SECONDS, clock=time.monotonic):
|
||||
self.ttl = ttl
|
||||
self._clock = clock
|
||||
self._user: tuple[float, AppUser] | None = None
|
||||
self._forms: dict[int, float] = {}
|
||||
|
||||
def get_user(self, user_id: int) -> AppUser | None:
|
||||
if self._user is None:
|
||||
return None
|
||||
ts, user = self._user
|
||||
if user.id != user_id or self._clock() - ts >= self.ttl:
|
||||
self._user = None
|
||||
return None
|
||||
return user
|
||||
|
||||
def set_user(self, user: AppUser | None) -> None:
|
||||
if user is not None:
|
||||
self._user = (self._clock(), user)
|
||||
|
||||
def form_access_ok(self, form_id: int) -> bool:
|
||||
ts = self._forms.get(form_id)
|
||||
if ts is None:
|
||||
return False
|
||||
if self._clock() - ts >= self.ttl:
|
||||
self._forms.pop(form_id, None)
|
||||
return False
|
||||
return True
|
||||
|
||||
def set_form_access(self, form_id: int) -> None:
|
||||
self._forms[form_id] = self._clock()
|
||||
|
||||
|
||||
class FormEventProcess:
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.sheet_service: SheetService = SheetService(db)
|
||||
self.bf_service: BudgetFormService = BudgetFormService(db)
|
||||
self.bl_service: BudgetLineService = BudgetLineService(db)
|
||||
self.user_service: UserService = UserService(db)
|
||||
self._cache: ConnectionCache | None = None
|
||||
|
||||
async def _check_form_access(self, form_id: int, user: AppUser) -> bool:
|
||||
"""Доступ пользователя к форме; положительный результат кэшируется."""
|
||||
cache = self._cache
|
||||
if cache and cache.form_access_ok(form_id):
|
||||
return True
|
||||
form = await self.bf_service.get(budget_form_id=form_id, user=user)
|
||||
if not form:
|
||||
return False
|
||||
if cache:
|
||||
cache.set_form_access(form_id)
|
||||
return True
|
||||
|
||||
async def process(
|
||||
self,
|
||||
@ -380,8 +438,14 @@ class FormEventProcess:
|
||||
user_id: int,
|
||||
sheet: str,
|
||||
direction: str | None = None,
|
||||
cache: ConnectionCache | None = None,
|
||||
) -> int | bool | dict:
|
||||
curr_user: AppUser = await self.user_service.get(user_id)
|
||||
self._cache = cache
|
||||
curr_user: AppUser | None = cache.get_user(user_id) if cache else None
|
||||
if curr_user is None:
|
||||
curr_user = await self.user_service.get(user_id)
|
||||
if cache:
|
||||
cache.set_user(curr_user)
|
||||
|
||||
match event_data["event"]:
|
||||
case "cell_updated":
|
||||
@ -458,8 +522,7 @@ class FormEventProcess:
|
||||
"value": any,
|
||||
}
|
||||
"""
|
||||
form = await self.bf_service.get(budget_form_id=form_id, user=user)
|
||||
if not form:
|
||||
if not await self._check_form_access(form_id=form_id, user=user):
|
||||
return None
|
||||
|
||||
return await self.sheet_service.update_cell(
|
||||
@ -495,8 +558,7 @@ class FormEventProcess:
|
||||
contract_end_date: Optional[datetime] = None
|
||||
}
|
||||
"""
|
||||
form = await self.bf_service.get(budget_form_id=form_id, user=user)
|
||||
if not form:
|
||||
if not await self._check_form_access(form_id=form_id, user=user):
|
||||
return None
|
||||
|
||||
ids = await self.bl_service.get_ids(budget_form_id=form_id)
|
||||
@ -617,8 +679,7 @@ class FormEventProcess:
|
||||
"row_id": int,
|
||||
}
|
||||
"""
|
||||
form = await self.bf_service.get(budget_form_id=form_id, user=user)
|
||||
if not form:
|
||||
if not await self._check_form_access(form_id=form_id, user=user):
|
||||
return None
|
||||
result = await self.sheet_service.delete_line(
|
||||
form_id=form_id,
|
||||
@ -970,6 +1031,8 @@ async def process_websocket(
|
||||
)
|
||||
return
|
||||
|
||||
conn_cache = ConnectionCache()
|
||||
|
||||
while True:
|
||||
data = loads(await websocket.receive_text())
|
||||
|
||||
@ -1068,10 +1131,13 @@ async def process_websocket(
|
||||
processor = processor_cls(db)
|
||||
t0 = time.perf_counter()
|
||||
data_old = data.copy()
|
||||
process_kwargs = dict(kwargs_process)
|
||||
if processor_cls is FormEventProcess:
|
||||
process_kwargs["cache"] = conn_cache
|
||||
data["result"] = await processor.process(
|
||||
event_data=data,
|
||||
user_id=user_id,
|
||||
**kwargs_process
|
||||
**process_kwargs
|
||||
)
|
||||
should_refresh_ahr = (
|
||||
con_key == ConnectionKeyEnum.FORM
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user