简体中文 English 繁體中文 日本語 Русский
本文为静态镜像,内容以交互版为准 在交互式文档中心打开 →

Пул соединений и трансляция

Соединения (WebSocket-сервера / SSE / WebSocket-клиентские исходящие) в ErisPulse являются первоклассными ресурсами на уровне фреймворка: при установке соединения автоматически регистрируются в единой таблице, поддерживают трансляцию, подписку на бизнес-группы и передачу/повторное использование между модулями, при выгрузке модуля автоматически закрываются и освобождаются фреймворком — бизнес-логика больше не должна создавать _ws_clients коллекции, и не будет утечек из-за забытого удаления.

Примеры в документации используют рекомендуемый синтаксис корневого импорта (рекомендуется с версии 2.10+): from ErisPulse import connections, router, client, WebSocketConnection

Обзор

from ErisPulse import connections, router, WebSocketConnection

# Сервер: соединение автоматически регистрируется, handler добавляет в бизнес-группу
@router.ws("MyModule", "/ws")
async def handle(ws: WebSocketConnection):
    ws.join_group("room:1")            # Название группы полностью определяется бизнес-логикой
    async for msg in ws.iter_text():
        ...                             # При разрыве соединения фреймворк автоматически отменяет регистрацию и очищает группу

# В любом месте: трансляция в группу (возвращает подробности успеха/неудачи)
result = await connections.broadcast({"type": "notify"}, group="room:1")
if not result.ok:
    ...                                 # result.failed: {id соединения: исключение}

Три вида соединений объединяются в единую таблицу регистрации:

kind Источник Владелец owner Описание
server @router.ws(...) / register_websocket(...) Модуль, зарегистрировавший маршрут При разрыве автоматически отменяется регистрация
sse @router.sse(...) / register_sse(...) Модуль, зарегистрировавший маршрут Трансляция через sse.send()
client await client.ws_connect(url) Текущий модуль (или явно указанный owner=) При закрытии/разрыве удалённой стороны автоматически отменяется регистрация; при остановке адаптера фреймворк закрывает все соединения

Для отключения автоматической регистрации: передайте track=False в маршрут или при исходящем соединении, старый код будет работать без изменений.

Просмотр пула соединений

from ErisPulse import connections

# Просмотр пула соединений модуля ("Сколько активных соединений сейчас у модуля?")
conns = connections.list(namespace="Dashboard")
for conn in conns:
    print(conn.id, conn.kind, conn.groups, conn.meta)

# Фильтрация по другим параметрам (все условия AND)
connections.list(owner="MyAdapter", kind="client")
connections.list(group="tenant:acme")

# Общая статистика (подсчёт по всем измерениям)
stats = connections.stats()
# {'total': 5, 'by_kind': {...}, 'by_namespace': {...}, 'by_owner': {...}, 'groups': {...}}

# Подсчёт аудита принадлежности (модуль ownership также доступен)
from ErisPulse import ownership
ownership.counts("Dashboard")   # {'connections': 3, ...}

У каждого соединения можно получить идентификационную информацию: conn.id (вроде Dashboard:1a2b3c4d), conn.namespace, conn.owner, conn.kind, conn.groups, conn.meta (свободный словарь бизнес-логики).

Трансляция и подписка

Группы (модель подписки)

Группы — это плоские строки, их именование полностью определяется бизнес-логикой (комнаты / арендаторы / темы...), вложенные уровни выражаются соглашениями в именах (например tenant:acme/room:1), фреймворк не ограничивает формат.

Операции возможны в двух направлениях:

# Направление 1: соединение активно присоединяется/отключается (внутри handler)
@router.ws("Chat", "/ws")
async def handle(ws: WebSocketConnection):
    ws.join_group("room:1")        # Эквивалентно connections.assign(ws.id, "room:1")
    ws.leave_group("room:1")

# Направление 2: управление пассивно распределяет (внутри маршрута / других модулях)
connections.assign(conn_id, "tenant:acme")
connections.dismiss(conn_id, "tenant:acme")   # Без указания группы = выход из всех групп

При разрыве соединения автоматически выходят из всех групп, бизнес-логика не должна очищать.

Трансляция

result = await connections.broadcast(
    data,                       # Для WS используется send_json; для SSE — send (автоматическая сериализация JSON)
    namespace="Dashboard",      # Фильтр по пространству имён
    group="room:1",             # Фильтр по группе
    kind="sse",                 # Фильтр по типу соединения
    owner="MyAdapter",          # Фильтр по владельцу
    ids={conn_id, ...},         # Явное указание кандидатов (фильтры всё ещё действуют)
    exclude={conn_id},          # Исключение некоторых соединений
    timeout=10.0,               # Таймаут отправки на одно соединение (не общий)
    concurrency=64,             # Максимальное количество одновременных отправок
    raise_on_error=False,       # При True выбрасывается первое исключение при неудаче
)
result.total      # Общее количество целевых соединений (исключая предварительно)
result.sent       # Список успешно отправленных соединений
result.failed     # {id соединения: исключение} (таймаут/разрыв/ошибка отправки)
result.excluded   # Исключённые соединения
result.ok         # Успешна ли отправка всем

Фильтры между собой работают по правилу AND; неудача одного соединения не влияет на другие; соединения из ids, которых уже не существует, учитываются в failed (чтобы бизнес-логика знала полную картину).

События жизненного цикла

При регистрации/удалении соединения/изменении групп генерируются стандартные события жизненного цикла, которые могут подписываться Dashboard и другие, для отображения в реальном времени:

from ErisPulse import lifecycle

@lifecycle.on("connection.registered")
async def on_registered(data): ...
@lifecycle.on("connection.unregistered")
async def on_unregistered(data): ...
@lifecycle.on("connection.group.joined")
async def on_joined(data): ...
@lifecycle.on("connection.group.left")
async def on_left(data): ...

Передача / повторное использование между модулями

Соединение является зарегистрированным ресурсом фреймворка, любой модуль может получить его по ID и повторно использовать — не нужно создавать новое соединение, и не нужно знать, кто его создал:

from ErisPulse import connections

conn = connections.get("Dashboard:1a2b3c4d")   # Если не существует/разорвано выбрасывается ConnectionNotFoundError
await conn.send_json({"ping": 1})              # Любой модуль может отправить
conn.join_group("someone-elses-room")          # Также может участвовать в группах

Принадлежность и права закрытия:

Объяснение: проверка прав закрытия зависит от контекста владельца во время выполнения (например owner_scope / событийная диспетчеризация / spawn_background и т.д. автоматически передают). Вызовы без контекста принадлежности (старый код без контекста) разрешаются, чтобы не нарушить работу; новые вызовы в контексте владельца будут защищены.

Исходящие соединения (клиентская сторона)

from ErisPulse import client

ws = await client.ws_connect("wss://example.com/ws")
ws.id          # Уже зарегистрирован, доступен для поиска/трансляции через connections
ws.owner       # Текущий модуль (при недоступности контекста владельца рекомендуется явно указать)

# При недоступности контекста владельца (например, в обратном вызове из потока инструментов) явно указать, чтобы при выгрузке могло автоматически освободиться:
ws = await client.ws_connect("wss://example.com/ws", owner="MyAdapter")

При активном разрыве удалённой стороны, локальном close() или выгрузке модуля автоматически отменяется регистрация; с момента выгрузки старые способы создания и управления соединениями без регистрации и очистки устарели.

Передача между потоками

Для доступа к соединению из подпотока (трансляция / отправка / закрытие) используйте стандартные средства фреймворка для возврата в главный цикл, не используйте вручную run_coroutine_threadsafe:

from ErisPulse import run_main_loop, spawn_later, spawn_thread

# Блокирующий вызов из подпотока:
run_main_loop(connections.broadcast({"tick": 1}, group="room:1"), timeout=5)

# Фоновый поток + задержанная задача (принадлежность владельца, автоматически отменяется при выгрузке):
def push_tick():
    return connections.broadcast({"tick": 1}, group="room:1")
spawn_later(30, push_tick, owner="MyModule")

Семантика выгрузки и очистки

Цепочка возврата принадлежности при выгрузке модуля / остановке адаптера последовательно: отмена фоновых задач и таймеров принадлежащих владельцу → закрытие и отмена регистрации принадлежащих соединений → вызов хука on_cleanup → отмена регистрации маршрутов/обработчиков и других ресурсов. Подробнее см. Система принадлежности (owner).

Warning

Начиная с версии 2.10, выгрузка модуля приводит к закрытию его зарегистрированных WS/SSE/исходящих соединений. Компоненты, зависящие от "сохранения соединений после выгрузки" (например, старая версия Dashboard-отправки), должны быть осведомлены — это ожидаемое поведение для предотвращения утечек соединений.

Справочник API

API Описание
connections.get(id) Получение соединения (вход между модулями), выбрасывает ConnectionNotFoundError при отсутствии
connections.list(namespace=, owner=, group=, kind=) Фильтрация и поиск списка соединений
connections.stats(namespace=, owner=) Подсчёт статистики по измерениям
connections.assign(id, *groups) / dismiss(id, *groups) Добавление/удаление групп со стороны управления
await connections.broadcast(data, ...) Трансляция, возвращает BroadcastResult
await connections.close(id, force=False) Закрытие и отмена регистрации (с проверкой прав)
await connections.close_owner(owner) Закрытие и отмена регистрации всех соединений владельца (вызывается в цепочке выгрузки)
conn.join_group(*groups) / conn.leave_group(*groups) Присоединение/выход из групп со стороны соединения
conn.send_json / send_text / iter_text / ... Отправка и приём (интерфейсы сервера/клиента одинаковы)
conn.meta Словарь бизнес-метаданных
conn.close(force=False) Закрытие (не владелец выбрасывает ConnectionPermissionError)