Загрузка данных


арх конст
    FILTERING_TIMEOUT: int | float = 30.0
    # ===== Фоновая подписка-keeper консьюмера api-gateway (ws_keepalive) =====
    KEEPALIVE_SUBSCRIPTION_REQUEST: str = "subscribeAllLeaksInfoRequest"
    KEEPALIVE_RECONNECT_INTERVAL_SECONDS: float = 5.0  # пауза перед повторной подпиской после обрыва
    KEEPALIVE_POLL_INTERVAL_SECONDS: float = 5.0  # периодичность проверки соединения и stop-сигнала
    KEEPALIVE_QUEUE_CLEAN_INTERVAL_SECONDS: float = 300.0  # как часто чистить очередь входящих сообщений
    KEEPALIVE_STOP_JOIN_TIMEOUT_SECONDS: float = 5.0  # таймаут join() фонового потока при остановке















вс кли
    @property
    def invocation_id(self):
        return self._invocation_id

    @property
    def is_connected(self) -> bool:
        """
        True, если wss-соединение установлено и приём сообщений работает.

        Соединение считается живым, пока открыт сокет и фоновый приёмник
        (_recv_task) не завершился. При обрыве связи _recv_loop возвращается,
        и задача переходит в состояние done - это признак потери соединения.
        """
        if self._ws is None:
            return False
        if self._recv_task is None:
            return False
        return not self._recv_task.done()














конфтест
        "vault_rejection_enabled": False,
        "keepalive": None,




    logger.info("[TEARDOWN] LDS Configurator очистка после ошибки setup набора")
    stop_background_subscription(cfg)





    if current_test_suite != cfg["current_suite"]:
        # stop old
        stop_background_subscription(cfg)





        # Сохраняем время старта имитатора для расчёта интервалов утечек в тестах
        cfg["imitator_start_time"] = stand_manager.start_time

        # Запускаем фоновую подписку-keeper консьюмера api-gateway после старта
        # lds-core и до проверки включённого ТУ в BasicInfo/MainPage: пока в прогоне
        # есть хотя бы один подписчик, консьюмер не выключается и не копит лаг в очереди.
        start_background_subscription(cfg)







    if next_suite != cfg["current_suite"]:
        stop_background_subscription(cfg)







        group_state = getattr(session.config, "group_state", {})
        stop_background_subscription(group_state)



















utils/helpers/ws_keepalive.py
"""
Фоновая подписка-keeper консьюмера api-gateway.

Проблема, которую решает модуль: api-gateway создаёт консьюмера очереди,
только пока есть хотя бы один ws-подписчик. В автотестах клиент подключается
на 10 секунд и отключается, поэтому консьюмер почти всегда выключен, сообщения
копятся в очереди, и к концу прогона накопленный лаг не успевает разгрестись
за окно ретраев.
"""

from __future__ import annotations

import asyncio
import logging
import threading
import time

from constants.architecture_constants import WebSocketClientConstants as WS_Const
from utils.helpers.pytest_auth import init_ws_stand_client

logger = logging.getLogger(__name__)


def start_background_subscription(group_state: dict) -> None:
    """
    Запускает фоновую подписку в отдельном daemon-потоке.

    Идемпотентна: повторный вызов при уже запущенном keeper ничего не делает.
    Состояние (поток + stop-сигнал) хранится в group_state["keepalive"].
    """
    if group_state.get("keepalive"):
        return

    stop_event = threading.Event()
    thread = threading.Thread(
        target=_keepalive_thread,
        args=(group_state, stop_event),
        name="ws-keepalive",
        daemon=True,
    )
    group_state["keepalive"] = {"thread": thread, "stop_event": stop_event}
    thread.start()


def stop_background_subscription(group_state: dict) -> None:
    """
    Останавливает фоновую подписку. Идемпотентна.

    Ставит stop-сигнал и ждёт завершения потока ограниченное время. Поскольку
    поток daemon-поток, даже при таймауте join pytest не зависнет.
    """
    keepalive = group_state.get("keepalive")
    if not keepalive:
        return

    stop_event = keepalive.get("stop_event")
    thread = keepalive.get("thread")

    if stop_event is not None:
        stop_event.set()
    if thread is not None and thread.is_alive():
        thread.join(timeout=WS_Const.KEEPALIVE_STOP_JOIN_TIMEOUT_SECONDS)

    group_state["keepalive"] = None
    logger.info("[KEEPER] фоновая подписка остановлена")


def _keepalive_thread(group_state: dict, stop_event: threading.Event) -> None:
    """Запускает асинхронный keeper-цикл в собственном event loop."""
    try:
        asyncio.run(_keepalive_async(group_state, stop_event))
    except Exception:
        logger.exception("[KEEPER] неожиданная ошибка фоновой подписки")


async def _keepalive_async(group_state: dict, stop_event: threading.Event) -> None:
    """
    Цикл фоновой подписки: подключение -> подписка -> слив очереди.

    При обрыве соединения или ошибке создаёт новый WebSocketClient и
    подписывается заново, пока не выставлен stop-сигнал.
    """
    while not stop_event.is_set():
        ws_client = init_ws_stand_client(group_state)
        ws_client.suppress_recv_logging = True
        try:
            async with ws_client as client:
                await client.invoke(WS_Const.KEEPALIVE_SUBSCRIPTION_REQUEST, [])
                logger.info(
                    "[KEEPER] фоновая подписка запущена: %s",
                    WS_Const.KEEPALIVE_SUBSCRIPTION_REQUEST,
                )
                last_clean = time.monotonic()
                while not stop_event.is_set():
                    await asyncio.sleep(WS_Const.KEEPALIVE_POLL_INTERVAL_SECONDS)
                    if stop_event.is_set():
                        break
                    if time.monotonic() - last_clean >= WS_Const.KEEPALIVE_QUEUE_CLEAN_INTERVAL_SECONDS:
                        client.clear_queue()
                        last_clean = time.monotonic()
                    if not client.is_connected:
                        logger.warning("[KEEPER] соединение потеряно, переподключаемся")
                        break
        except asyncio.CancelledError:
            break
        except Exception as error:
            if stop_event.is_set():
                break
            logger.warning(
                "[KEEPER] ошибка соединения/подписки: %s. Повтор через %s с",
                error,
                WS_Const.KEEPALIVE_RECONNECT_INTERVAL_SECONDS,
            )
            await asyncio.sleep(WS_Const.KEEPALIVE_RECONNECT_INTERVAL_SECONDS)