Files
ros_control/app/services/poller.py
T
ayurishchevandClaude Opus 5.5 ba8ac7be71 Кэш списка бакета, БД без блокировки event loop, один процесс на БД
Производительность и масштабирование, пункты 8–10 ревью (docs/changes/019):
- кэш списка бакета и связей файлов с копиями (BACKUPS_CACHE_TTL, 60 с):
  sync_rows только при реальном чтении бакета; сброс после бэкапа и
  удаления, счётчик поколений против гонки; refresh=1 и «Обновить список»;
- обработчики API/UI без await — обычные функции (пул потоков FastAPI),
  запуск задач остаётся async; запись статуса, задачи и sync_rows —
  через asyncio.to_thread; SQLite: WAL, synchronous=NORMAL, busy_timeout;
- файловая блокировка <файл БД>.lock: второй процесс на той же БД не
  стартует; раздел «Ограничения» в README.

Тесты: 24 из 24. Стенд (порт 8001): кэш 0,014 с против 0,138 с, второй
uvicorn на боевой БД отклонён блокировкой, боевые данные не изменены.
Ручная проверка UI пользователем на момент коммита не подтверждена.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 12:18:32 +03:00

47 lines
1.8 KiB
Python

"""Фоновый опрос устройств: быстрое обнаружение недоступности (period = POLL_INTERVAL)."""
import asyncio
import logging
import time
from app.config import get_settings
from app.services import devices, events, ops
log = logging.getLogger("ros_control.poller")
async def cycle(last_full: dict[str, float]) -> None:
"""Один проход по всем устройствам (параллельно, не более MAX_CONCURRENCY одновременно)."""
s = get_settings()
ids = [d.id for d in await asyncio.to_thread(devices.list_devices)]
for gone in set(last_full) - set(ids):
del last_full[gone]
sem = asyncio.Semaphore(s.max_concurrency)
started = time.monotonic()
async def one(device_id: str) -> None:
async with sem:
full = started - last_full.get(device_id, float("-inf")) >= s.update_check_interval
try:
await ops.poll_device(device_id, full)
except LookupError: # удалили во время цикла
return
except Exception: # noqa: BLE001 — сбой одного устройства не должен ронять опрос остальных
log.exception("poll failed: device %s", device_id)
return
if full:
last_full[device_id] = time.monotonic()
await asyncio.gather(*(one(i) for i in ids))
async def run() -> None:
events.set_actor("poller")
last_full: dict[str, float] = {}
interval = get_settings().poll_interval
while True:
try:
await cycle(last_full)
except Exception: # noqa: BLE001
log.exception("poll cycle failed")
await asyncio.sleep(interval)