diff --git a/app/src/market_data/acquisition/runtime/transport_messages.py b/app/src/market_data/acquisition/runtime/transport_messages.py new file mode 100644 index 0000000..5bf8c57 --- /dev/null +++ b/app/src/market_data/acquisition/runtime/transport_messages.py @@ -0,0 +1,29 @@ +# app/src/market_data/acquisition/runtime/transport_messages.py + +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True, slots=True) +class TransportTextMessage: + """ + Транспортное текстовое сообщение. + + Представляет уже полученный или подготовленный для отправки + текстовый payload без какой-либо интерпретации содержимого. + """ + + payload: str + + +@dataclass(frozen=True, slots=True) +class TransportBinaryMessage: + """ + Транспортное бинарное сообщение. + + Представляет уже полученный или подготовленный для отправки + бинарный payload без какой-либо интерпретации содержимого. + """ + + payload: bytes \ No newline at end of file diff --git a/app/tests/unit/market_data/acquisition/runtime/test_transport_messages.py b/app/tests/unit/market_data/acquisition/runtime/test_transport_messages.py new file mode 100644 index 0000000..081ac6a --- /dev/null +++ b/app/tests/unit/market_data/acquisition/runtime/test_transport_messages.py @@ -0,0 +1,36 @@ +# app/tests/unit/market_data/acquisition/runtime/test_transport_messages.py + +from dataclasses import FrozenInstanceError + +import pytest + +from src.market_data.acquisition.runtime.transport_messages import ( + TransportBinaryMessage, + TransportTextMessage, +) + + +def test_transport_text_message() -> None: + message = TransportTextMessage(payload='{"ping":1}') + + assert message.payload == '{"ping":1}' + + +def test_transport_binary_message() -> None: + message = TransportBinaryMessage(payload=b"\x01\x02") + + assert message.payload == b"\x01\x02" + + +def test_text_message_is_immutable() -> None: + message = TransportTextMessage(payload="abc") + + with pytest.raises(FrozenInstanceError): + setattr(message, "payload", "def") + + +def test_binary_message_is_immutable() -> None: + message = TransportBinaryMessage(payload=b"\x00") + + with pytest.raises(FrozenInstanceError): + setattr(message, "payload", b"\x01") \ No newline at end of file diff --git a/docs/migrations/build_059_10.md b/docs/migrations/build_059_10.md new file mode 100644 index 0000000..d245355 --- /dev/null +++ b/docs/migrations/build_059_10.md @@ -0,0 +1,423 @@ +# Build 059.10 — Runtime Transport Message Models + +**Migration Build** + +--- + +# Цель Build + +После завершения Build 059.9 в проекте появился самостоятельный Runtime слой, содержащий транспортные контракты: + +- WebSocketTransportProtocol +- WebSocketSessionProtocol +- WebSocketSubscriptionManagerProtocol + +Следующим логическим шагом является создание моделей данных, которыми будут обмениваться компоненты Runtime. + +Данный Build вводит первый уровень моделей Runtime — **Transport Message Models**. + +--- + +# Причина появления Transport Message Models + +До настоящего момента Runtime содержал только интерфейсы взаимодействия. + +Например: + +``` +Transport + │ +connect() +disconnect() +send() +receive() +``` + +Однако отсутствовало описание того, **какие именно объекты транспорт должен принимать и возвращать**. + +Использование непосредственно типов + +```python +str +bytes +``` + +делает код менее выразительным. + +Невозможно определить: + +- транспортное сообщение это; +- сериализованный JSON; +- бинарный protobuf; +- внутренний объект Runtime. + +Поэтому вводится отдельный уровень моделей транспортных сообщений. + +--- + +# Архитектурная идея + +Главная задача данного Build — отделить транспортные данные от всех остальных сущностей системы. + +Получается следующая иерархия. + +```text +Transport + │ +Transport Message + │ +Parser + │ +Validation + │ +Mapper + │ +Canonical Models +``` + +Transport знает только одно: + +> существует некоторый payload. + +Он совершенно не знает: + +- что находится внутри payload; +- как этот payload будет интерпретирован; +- является ли он JSON; +- относится ли он к конкретной бирже; +- какие модели будут построены дальше. + +Это полностью соответствует принципу Single Responsibility. + +--- + +# Почему Runtime не знает про WebSocket + +Несмотря на то, что текущим транспортом является WebSocket, Runtime проектируется максимально универсальным. + +Поэтому модели транспортных сообщений намеренно не содержат слов: + +- WebSocket +- Dzengi +- Exchange +- Candle +- Quote + +Runtime оперирует исключительно транспортными сообщениями. + +Это позволяет в будущем использовать один и тот же Runtime для различных транспортов: + +- WebSocket; +- HTTP Streaming; +- FIX; +- gRPC Streaming; +- внутренние очереди сообщений. + +--- + +# Новые модели + +Создан новый файл + +``` +src/market_data/acquisition/runtime/transport_messages.py +``` + +В Build добавлены две immutable модели. + +--- + +## TransportTextMessage + +Представляет текстовое транспортное сообщение. + +Содержит единственное поле: + +``` +payload: str +``` + +Никакой дополнительной информации модель не содержит. + +Она не знает: + +- что внутри находится JSON; +- является ли сообщение командой; +- является ли сообщение событием; +- содержит ли сообщение свечу. + +Она является исключительно контейнером транспортного текста. + +--- + +## TransportBinaryMessage + +Представляет бинарное транспортное сообщение. + +Содержит единственное поле + +``` +payload: bytes +``` + +Как и текстовая модель, не содержит никакой информации о содержимом. + +Runtime рассматривает бинарные данные как непрозрачный набор байтов. + +--- + +# Почему модели содержат только payload + +Во время проектирования рассматривались варианты добавить дополнительные поля. + +Например: + +``` +message_type +encoding +channel +metadata +timestamp +``` + +От данной идеи было принято решение отказаться. + +Причины следующие. + +## Причина №1 + +Транспорт не должен ничего знать о содержимом сообщения. + +Если транспорт начинает анализировать содержимое сообщения, происходит смешение ответственности между Runtime и Parser. + +--- + +## Причина №2 + +Любые дополнительные поля являются предположениями относительно будущего транспорта. + +На данном этапе архитектуры неизвестно: + +- понадобится ли message_type; +- понадобится ли encoding; +- понадобится ли channel; +- понадобится ли metadata. + +Следовательно, преждевременно добавлять подобные поля. + +--- + +## Причина №3 + +Минимальные immutable модели проще поддерживать. + +При необходимости они могут быть расширены отдельным Build без нарушения обратной совместимости. + +--- + +# Почему здесь отсутствуют ConnectRequest + +Во время проектирования рассматривалась идея добавить модели: + +``` +ConnectRequest +DisconnectRequest +SubscribeRequest +UnsubscribeRequest +``` + +Однако было принято решение отказаться от неё. + +Причина заключается в разделении уровней ответственности. + +ConnectRequest не является транспортным сообщением. + +Это команда Runtime. + +--- + +# Почему отсутствуют Ping/Pong + +По аналогичной причине. + +Ping и Pong являются частью логики Runtime. + +Транспорт получает некоторый payload. + +Что именно находится внутри этого payload, транспорт не анализирует. + +Следовательно: + +Ping/Pong должны появиться позднее вместе с Runtime Commands. + +--- + +# Новая архитектура Runtime + +После завершения Build Runtime начинает разделяться на несколько независимых уровней. + +```text +Runtime + +Transport Protocol + │ +Transport Messages + │ +Runtime Commands + │ +Runtime Events + │ +Runtime Services +``` + +Подобное разделение является значительно более устойчивым, чем смешение всех сущностей в одном модуле. + +--- + +# Обновлённый план Runtime + +После выполнения Build 059.10 дальнейшее развитие Runtime выглядит следующим образом. + +## Build 059.11 + +Runtime Commands + +Будут введены команды: + +- ConnectCommand +- DisconnectCommand +- SubscribeCommand +- UnsubscribeCommand +- SendTextCommand +- SendBinaryCommand + +--- + +## Build 059.12 + +Runtime Events + +Появятся события Runtime: + +- ConnectedEvent +- DisconnectedEvent +- SubscriptionRestoredEvent +- HeartbeatTimeoutEvent +- ReconnectStartedEvent +- ReconnectCompletedEvent + +--- + +## Build 059.13 + +Subscription Manager + +Полноценное управление активными подписками Runtime. + +--- + +## Build 059.14 + +WebSocket Session + +Жизненный цикл Runtime Session. + +--- + +## Build 059.15 + +Transport Reliability + +Инфраструктура Runtime: + +- Heartbeat +- Reconnect +- Ping/Pong +- Scheduler +- Recovery + +--- + +## Build 059.16 + +Runtime Integration + +Интеграция нового Runtime с существующей цепочкой Acquisition. + +--- + +## Build 059.17 + +Runtime Validation & Documentation + +Полная регрессия новой архитектуры и финальная документация. + +--- + +# Проверка Build + +Выполнены проверки. + +## Компиляция + +``` +python -m compileall +``` + +Успешно. + +--- + +## Unit Tests + +``` +test_transport_messages.py +``` + +Все тесты успешно пройдены. + +Проверяется: + +- создание моделей; +- корректность хранения payload; +- immutable поведение dataclass. + +--- + +## Runtime Regression + +Проверена совместимость новой модели сообщений с ранее созданными Runtime Protocol. + +Все проверки успешно завершены. + +--- + +## Проверка репозитория + +Выполнен + +``` +git diff --check +``` + +Ошибок форматирования не обнаружено. + +--- + +# Итог + +Build 059.10 завершает создание базового транспортного уровня Runtime. + +После него Runtime уже содержит: + +- транспортные контракты; +- транспортные модели сообщений. + +Следующие Build будут постепенно добавлять поведение системы (Commands, Events, Session, Subscription Manager, Reconnect), не изменяя уже построенный фундамент. + +Таким образом продолжается основной принцип миграции Dzentra: + +- каждый Build решает одну архитектурную задачу; +- изменения являются минимальными и независимыми; +- существующая цепочка Acquisition остаётся неизменной; +- работающий торговый бот продолжает функционировать без необходимости одновременной полной миграции. \ No newline at end of file