Загрузка данных
вс сейшн
"""
Постоянная ws-сессия на датасет, имитирующая подключение фронта.
"""
from __future__ import annotations
import asyncio
import logging
import uuid
import requests
from clients.websocket_client import WebSocketClient
from constants.architecture_constants import HTTPClientConstants as HttpConst
from constants.architecture_constants import StandConstants
from constants.architecture_constants import WebSocketClientConstants as WS_Const
logger = logging.getLogger(__name__)
class WsSessionManager:
"""Управляет постоянным ws-соединением датасета: подписки, keepalive, реконнект."""
def __init__(self, group_state: dict, client: WebSocketClient) -> None:
self._group_state = group_state
self._client = client
self._stop_event = asyncio.Event()
self._generation = group_state.get("dataset_generation")
self._correlation_id = str(uuid.uuid4())
self._supervisor_task: asyncio.Task | None = None
self._rest_ping_task: asyncio.Task | None = None
self._heartbeat_task: asyncio.Task | None = None
self._rest_ping_count = 0
self._last_received = 0
self._last_sent_ping = 0
self._last_rest_ping = 0
async def start(self) -> None:
"""Подписывается на bundle и запускает фоновые задачи."""
await self._subscribe_bundle()
self._supervisor_task = asyncio.create_task(self._supervisor_loop())
self._rest_ping_task = asyncio.create_task(self._rest_ping_loop())
self._heartbeat_task = asyncio.create_task(self._heartbeat_loop())
logger.info("[WS_SESSION] сессия запущена (correlation_id=%s)", self._correlation_id)
async def stop(self) -> None:
"""Останавливает фоновые задачи."""
self._stop_event.set()
for task in (self._rest_ping_task, self._supervisor_task, self._heartbeat_task):
if task is not None:
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
logger.info("[WS_SESSION] сессия остановлена")
def _current_tu_id(self) -> int | None:
"""tuId для MainPage: resolved из Администрирования, либо legacy из маркера."""
return self._group_state.get("resolved_tu_id") or self._group_state.get("legacy_tu_id")
async def _subscribe_bundle(self) -> None:
"""Подписывается на bundle так же, как это делает фронт."""
tu_id = self._current_tu_id()
subscriptions = [
(WS_Const.SUBSCRIBE_PLAYER_STATUS_REQUEST, []),
(WS_Const.SUBSCRIBE_SELF_DIAGNOSTIC_REQUEST, []),
]
if tu_id is not None:
subscriptions.append(
(WS_Const.SUBSCRIBE_MAIN_PAGE_INFO_REQUEST, {"tuIds": [tu_id], "additionalProperties": None})
)
for invoke_type, invoke_params in subscriptions:
try:
await self._client.invoke(invoke_type, invoke_params)
except Exception as error:
logger.warning("[WS_SESSION] не удалось подписаться на %s: %s", invoke_type, error)
logger.info("[WS_SESSION] bundle-подписки оформлены (tu_id=%s)", tu_id)
async def _supervisor_loop(self) -> None:
"""Следит за состоянием соединения и сменой датасета."""
while not self._stop_event.is_set():
await asyncio.sleep(WS_Const.WS_SESSION_SUPERVISOR_INTERVAL_SECONDS)
if self._stop_event.is_set():
return
generation = self._group_state.get("dataset_generation")
if not self._client.is_connected or generation != self._generation:
logger.info(
"[WS_SESSION] переподключение: is_connected=%s, generation %s -> %s",
self._client.is_connected,
self._generation,
generation,
)
await self._reestablish(generation)
async def _reestablish(self, new_generation: int | None) -> None:
"""Переподключается с актуальными кредами и переподписывается на bundle."""
auth_token = self._group_state.get("auth_token")
x_user_id = self._group_state.get("x_user_id")
if not auth_token or not x_user_id:
logger.warning("[WS_SESSION] нет кредов для переподключения, пропускаю")
return
self._client.update_credentials(auth_token, x_user_id)
try:
await self._client.reconnect()
except Exception as error:
logger.warning("[WS_SESSION] не удалось переподключиться: %s", error)
return
self._generation = new_generation
await self._subscribe_bundle()
logger.info("[WS_SESSION] соединение переподключено, bundle переподписан")
async def _heartbeat_loop(self) -> None:
"""Раз в минуту логирует состояние сессии: соединение, очередь, активность."""
while not self._stop_event.is_set():
await asyncio.sleep(WS_Const.WS_SESSION_HEARTBEAT_INTERVAL_SECONDS)
if self._stop_event.is_set():
return
received = self._client.received_messages_count
sent_ping = self._client.sent_ping_count
rest_ping = self._rest_ping_count
logger.info(
"[WS_SESSION] heartbeat: активно=%s, очередь=%d, получено_за_минуту=%d (всего=%d), "
"signalr_ping=%d, rest_ping=%d",
self._client.is_connected,
self._client.recv_queue.qsize(),
received - self._last_received,
received,
sent_ping - self._last_sent_ping,
rest_ping - self._last_rest_ping,
)
self._last_received = received
self._last_sent_ping = sent_ping
self._last_rest_ping = rest_ping
async def _rest_ping_loop(self) -> None:
"""Периодически пингует REST /Ping с correlation-id."""
while not self._stop_event.is_set():
await asyncio.sleep(WS_Const.REST_PING_INTERVAL_SECONDS)
if self._stop_event.is_set():
return
await self._ping_rest()
async def _ping_rest(self) -> None:
"""Один REST-пинг. Ошибки (5xx/4xx/сеть) не роняют keepalive."""
stand_host = self._group_state.get("stand_host")
auth_token = self._group_state.get("auth_token")
if not stand_host or not auth_token:
logger.warning("[REST_PING] нет stand_host/auth_token, пропускаю")
return
url = f"https://{stand_host}{StandConstants.PING_URL_PATH}"
headers = {
HttpConst.X_SECURITY_SIGNATURE_KEY: auth_token,
HttpConst.X_TN_CORRELATION_ID_KEY: self._correlation_id,
}
self._rest_ping_count += 1
try:
response = await asyncio.to_thread(requests.post, url, headers=headers, timeout=10)
if not response.ok:
logger.warning(
"[REST_PING] %s -> %s: %s",
StandConstants.PING_URL_PATH,
response.status_code,
response.text[:200],
)
else:
logger.info("[REST_PING] %s -> %s", StandConstants.PING_URL_PATH, response.status_code)
except requests.RequestException as error:
logger.warning("[REST_PING] ошибка запроса %s: %s", StandConstants.PING_URL_PATH, error)
вс тест утилс
"""
Постоянная ws-сессия на датасет, имитирующая подключение фронта.
"""
from __future__ import annotations
import asyncio
import logging
import uuid
import requests
from clients.websocket_client import WebSocketClient
from constants.architecture_constants import HTTPClientConstants as HttpConst
from constants.architecture_constants import StandConstants
from constants.architecture_constants import WebSocketClientConstants as WS_Const
logger = logging.getLogger(__name__)
class WsSessionManager:
"""Управляет постоянным ws-соединением датасета: подписки, keepalive, реконнект."""
def __init__(self, group_state: dict, client: WebSocketClient) -> None:
self._group_state = group_state
self._client = client
self._stop_event = asyncio.Event()
self._generation = group_state.get("dataset_generation")
self._correlation_id = str(uuid.uuid4())
self._supervisor_task: asyncio.Task | None = None
self._rest_ping_task: asyncio.Task | None = None
self._heartbeat_task: asyncio.Task | None = None
self._rest_ping_count = 0
self._last_received = 0
self._last_sent_ping = 0
self._last_rest_ping = 0
async def start(self) -> None:
"""Подписывается на bundle и запускает фоновые задачи."""
await self._subscribe_bundle()
self._supervisor_task = asyncio.create_task(self._supervisor_loop())
self._rest_ping_task = asyncio.create_task(self._rest_ping_loop())
self._heartbeat_task = asyncio.create_task(self._heartbeat_loop())
logger.info("[WS_SESSION] сессия запущена (correlation_id=%s)", self._correlation_id)
async def stop(self) -> None:
"""Останавливает фоновые задачи."""
self._stop_event.set()
for task in (self._rest_ping_task, self._supervisor_task, self._heartbeat_task):
if task is not None:
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
logger.info("[WS_SESSION] сессия остановлена")
def _current_tu_id(self) -> int | None:
"""tuId для MainPage: resolved из Администрирования, либо legacy из маркера."""
return self._group_state.get("resolved_tu_id") or self._group_state.get("legacy_tu_id")
async def _subscribe_bundle(self) -> None:
"""Подписывается на bundle так же, как это делает фронт."""
tu_id = self._current_tu_id()
subscriptions = [
(WS_Const.SUBSCRIBE_PLAYER_STATUS_REQUEST, []),
(WS_Const.SUBSCRIBE_SELF_DIAGNOSTIC_REQUEST, []),
]
if tu_id is not None:
subscriptions.append(
(WS_Const.SUBSCRIBE_MAIN_PAGE_INFO_REQUEST, {"tuIds": [tu_id], "additionalProperties": None})
)
for invoke_type, invoke_params in subscriptions:
try:
await self._client.invoke(invoke_type, invoke_params)
except Exception as error:
logger.warning("[WS_SESSION] не удалось подписаться на %s: %s", invoke_type, error)
logger.info("[WS_SESSION] bundle-подписки оформлены (tu_id=%s)", tu_id)
async def _supervisor_loop(self) -> None:
"""Следит за состоянием соединения и сменой датасета."""
while not self._stop_event.is_set():
await asyncio.sleep(WS_Const.WS_SESSION_SUPERVISOR_INTERVAL_SECONDS)
if self._stop_event.is_set():
return
generation = self._group_state.get("dataset_generation")
if not self._client.is_connected or generation != self._generation:
logger.info(
"[WS_SESSION] переподключение: is_connected=%s, generation %s -> %s",
self._client.is_connected,
self._generation,
generation,
)
await self._reestablish(generation)
async def _reestablish(self, new_generation: int | None) -> None:
"""Переподключается с актуальными кредами и переподписывается на bundle."""
auth_token = self._group_state.get("auth_token")
x_user_id = self._group_state.get("x_user_id")
if not auth_token or not x_user_id:
logger.warning("[WS_SESSION] нет кредов для переподключения, пропускаю")
return
self._client.update_credentials(auth_token, x_user_id)
try:
await self._client.reconnect()
except Exception as error:
logger.warning("[WS_SESSION] не удалось переподключиться: %s", error)
return
self._generation = new_generation
await self._subscribe_bundle()
logger.info("[WS_SESSION] соединение переподключено, bundle переподписан")
async def _heartbeat_loop(self) -> None:
"""Раз в минуту логирует состояние сессии: соединение, очередь, активность."""
while not self._stop_event.is_set():
await asyncio.sleep(WS_Const.WS_SESSION_HEARTBEAT_INTERVAL_SECONDS)
if self._stop_event.is_set():
return
received = self._client.received_messages_count
sent_ping = self._client.sent_ping_count
rest_ping = self._rest_ping_count
logger.info(
"[WS_SESSION] heartbeat: активно=%s, очередь=%d, получено_за_минуту=%d (всего=%d), "
"signalr_ping=%d, rest_ping=%d",
self._client.is_connected,
self._client.recv_queue.qsize(),
received - self._last_received,
received,
sent_ping - self._last_sent_ping,
rest_ping - self._last_rest_ping,
)
self._last_received = received
self._last_sent_ping = sent_ping
self._last_rest_ping = rest_ping
async def _rest_ping_loop(self) -> None:
"""Периодически пингует REST /Ping с correlation-id."""
while not self._stop_event.is_set():
await asyncio.sleep(WS_Const.REST_PING_INTERVAL_SECONDS)
if self._stop_event.is_set():
return
await self._ping_rest()
async def _ping_rest(self) -> None:
"""Один REST-пинг. Ошибки (5xx/4xx/сеть) не роняют keepalive."""
stand_host = self._group_state.get("stand_host")
auth_token = self._group_state.get("auth_token")
if not stand_host or not auth_token:
logger.warning("[REST_PING] нет stand_host/auth_token, пропускаю")
return
url = f"https://{stand_host}{StandConstants.PING_URL_PATH}"
headers = {
HttpConst.X_SECURITY_SIGNATURE_KEY: auth_token,
HttpConst.X_TN_CORRELATION_ID_KEY: self._correlation_id,
}
self._rest_ping_count += 1
try:
response = await asyncio.to_thread(requests.post, url, headers=headers, timeout=10)
if not response.ok:
logger.warning(
"[REST_PING] %s -> %s: %s",
StandConstants.PING_URL_PATH,
response.status_code,
response.text[:200],
)
else:
logger.info("[REST_PING] %s -> %s", StandConstants.PING_URL_PATH, response.status_code)
except requests.RequestException as error:
logger.warning("[REST_PING] ошибка запроса %s: %s", StandConstants.PING_URL_PATH, error)
конфтест
"""
Постоянная ws-сессия на датасет, имитирующая подключение фронта.
"""
from __future__ import annotations
import asyncio
import logging
import uuid
import requests
from clients.websocket_client import WebSocketClient
from constants.architecture_constants import HTTPClientConstants as HttpConst
from constants.architecture_constants import StandConstants
from constants.architecture_constants import WebSocketClientConstants as WS_Const
logger = logging.getLogger(__name__)
class WsSessionManager:
"""Управляет постоянным ws-соединением датасета: подписки, keepalive, реконнект."""
def __init__(self, group_state: dict, client: WebSocketClient) -> None:
self._group_state = group_state
self._client = client
self._stop_event = asyncio.Event()
self._generation = group_state.get("dataset_generation")
self._correlation_id = str(uuid.uuid4())
self._supervisor_task: asyncio.Task | None = None
self._rest_ping_task: asyncio.Task | None = None
self._heartbeat_task: asyncio.Task | None = None
self._rest_ping_count = 0
self._last_received = 0
self._last_sent_ping = 0
self._last_rest_ping = 0
async def start(self) -> None:
"""Подписывается на bundle и запускает фоновые задачи."""
await self._subscribe_bundle()
self._supervisor_task = asyncio.create_task(self._supervisor_loop())
self._rest_ping_task = asyncio.create_task(self._rest_ping_loop())
self._heartbeat_task = asyncio.create_task(self._heartbeat_loop())
logger.info("[WS_SESSION] сессия запущена (correlation_id=%s)", self._correlation_id)
async def stop(self) -> None:
"""Останавливает фоновые задачи."""
self._stop_event.set()
for task in (self._rest_ping_task, self._supervisor_task, self._heartbeat_task):
if task is not None:
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
logger.info("[WS_SESSION] сессия остановлена")
def _current_tu_id(self) -> int | None:
"""tuId для MainPage: resolved из Администрирования, либо legacy из маркера."""
return self._group_state.get("resolved_tu_id") or self._group_state.get("legacy_tu_id")
async def _subscribe_bundle(self) -> None:
"""Подписывается на bundle так же, как это делает фронт."""
tu_id = self._current_tu_id()
subscriptions = [
(WS_Const.SUBSCRIBE_PLAYER_STATUS_REQUEST, []),
(WS_Const.SUBSCRIBE_SELF_DIAGNOSTIC_REQUEST, []),
]
if tu_id is not None:
subscriptions.append(
(WS_Const.SUBSCRIBE_MAIN_PAGE_INFO_REQUEST, {"tuIds": [tu_id], "additionalProperties": None})
)
for invoke_type, invoke_params in subscriptions:
try:
await self._client.invoke(invoke_type, invoke_params)
except Exception as error:
logger.warning("[WS_SESSION] не удалось подписаться на %s: %s", invoke_type, error)
logger.info("[WS_SESSION] bundle-подписки оформлены (tu_id=%s)", tu_id)
async def _supervisor_loop(self) -> None:
"""Следит за состоянием соединения и сменой датасета."""
while not self._stop_event.is_set():
await asyncio.sleep(WS_Const.WS_SESSION_SUPERVISOR_INTERVAL_SECONDS)
if self._stop_event.is_set():
return
generation = self._group_state.get("dataset_generation")
if not self._client.is_connected or generation != self._generation:
logger.info(
"[WS_SESSION] переподключение: is_connected=%s, generation %s -> %s",
self._client.is_connected,
self._generation,
generation,
)
await self._reestablish(generation)
async def _reestablish(self, new_generation: int | None) -> None:
"""Переподключается с актуальными кредами и переподписывается на bundle."""
auth_token = self._group_state.get("auth_token")
x_user_id = self._group_state.get("x_user_id")
if not auth_token or not x_user_id:
logger.warning("[WS_SESSION] нет кредов для переподключения, пропускаю")
return
self._client.update_credentials(auth_token, x_user_id)
try:
await self._client.reconnect()
except Exception as error:
logger.warning("[WS_SESSION] не удалось переподключиться: %s", error)
return
self._generation = new_generation
await self._subscribe_bundle()
logger.info("[WS_SESSION] соединение переподключено, bundle переподписан")
async def _heartbeat_loop(self) -> None:
"""Раз в минуту логирует состояние сессии: соединение, очередь, активность."""
while not self._stop_event.is_set():
await asyncio.sleep(WS_Const.WS_SESSION_HEARTBEAT_INTERVAL_SECONDS)
if self._stop_event.is_set():
return
received = self._client.received_messages_count
sent_ping = self._client.sent_ping_count
rest_ping = self._rest_ping_count
logger.info(
"[WS_SESSION] heartbeat: активно=%s, очередь=%d, получено_за_минуту=%d (всего=%d), "
"signalr_ping=%d, rest_ping=%d",
self._client.is_connected,
self._client.recv_queue.qsize(),
received - self._last_received,
received,
sent_ping - self._last_sent_ping,
rest_ping - self._last_rest_ping,
)
self._last_received = received
self._last_sent_ping = sent_ping
self._last_rest_ping = rest_ping
async def _rest_ping_loop(self) -> None:
"""Периодически пингует REST /Ping с correlation-id."""
while not self._stop_event.is_set():
await asyncio.sleep(WS_Const.REST_PING_INTERVAL_SECONDS)
if self._stop_event.is_set():
return
await self._ping_rest()
async def _ping_rest(self) -> None:
"""Один REST-пинг. Ошибки (5xx/4xx/сеть) не роняют keepalive."""
stand_host = self._group_state.get("stand_host")
auth_token = self._group_state.get("auth_token")
if not stand_host or not auth_token:
logger.warning("[REST_PING] нет stand_host/auth_token, пропускаю")
return
url = f"https://{stand_host}{StandConstants.PING_URL_PATH}"
headers = {
HttpConst.X_SECURITY_SIGNATURE_KEY: auth_token,
HttpConst.X_TN_CORRELATION_ID_KEY: self._correlation_id,
}
self._rest_ping_count += 1
try:
response = await asyncio.to_thread(requests.post, url, headers=headers, timeout=10)
if not response.ok:
logger.warning(
"[REST_PING] %s -> %s: %s",
StandConstants.PING_URL_PATH,
response.status_code,
response.text[:200],
)
else:
logger.info("[REST_PING] %s -> %s", StandConstants.PING_URL_PATH, response.status_code)
except requests.RequestException as error:
logger.warning("[REST_PING] ошибка запроса %s: %s", StandConstants.PING_URL_PATH, error)