40 KiB
Build 060.22 — Acquisition Runtime Service
Статус: Accepted
Тип документа: Architecture Specification
Build: 060.22
Ветка: Trades Feed (Time & Sales)
Документ: build_060_22_architecture.md
Связанные документы:
- Build 060.21 Architecture — архитектурная спецификация Runtime Integration Contracts;
- Build 060.21 Engineering Migration Report — предыдущий Build;
- Build 060.20.1 Architecture — спецификация владения состоянием Trade Stream.
Назначение документа
Настоящий документ является официальной архитектурной спецификацией Build 060.22 — Acquisition Runtime Service.
Документ определяет создание первого исполняемого сервисного компонента Runtime Layer подсистемы Market Data Acquisition.
Build 060.21 сформировал типизированную контрактную границу Runtime:
AcquisitionRuntimeCommand
AcquisitionRuntimeEvent
AcquisitionRuntimeCommandDispatcherProtocol
AcquisitionRuntimeEventPublisherProtocol
Однако созданные контракты пока не имеют производственной реализации.
В системе отсутствует компонент, который:
- принимает типизированные Runtime-команды;
- маршрутизирует их к соответствующим инфраструктурным зависимостям;
- публикует результирующие Runtime-события;
- формирует единую точку исполнения команд Acquisition Runtime.
Build 060.22 закрывает именно эту архитектурную задачу.
Документ служит единственным источником истины при реализации данного Build.
Изменение зафиксированных решений в процессе написания кода не допускается без отдельного архитектурного пересмотра.
Статус Build
Build 060.22 является следующим этапом серии Build 060, посвящённой формированию полноценной подсистемы Trades Feed (Time & Sales).
К моменту начала данного Build в проекте уже существуют:
- транспортные модели WebSocket;
- Runtime Commands;
- Runtime Events;
- WebSocket transport protocol;
- WebSocket session protocol;
- WebSocket subscription manager protocol;
- контракт диспетчеризации Runtime-команд;
- контракт публикации Runtime-событий;
- Canonical Trade Model;
- Trades Feed;
- Trade Stream Consistency;
- Trade Recovery;
- специализированное хранилище
TradeStreamStateStore.
При этом Runtime Layer пока состоит только из контрактов и моделей сообщений.
Фактический сервис исполнения Runtime-команд отсутствует.
Следовательно, Runtime ещё не является работающей подсистемой.
Build 060.22 вводит минимальную исполняемую реализацию сервисного уровня, не подключая её пока к Trades Feed и корневому Acquisition Service.
Контекст
После завершения Build 060.21 архитектура Runtime Protocol Layer выглядит следующим образом.
AcquisitionRuntimeCommand
│
▼
AcquisitionRuntimeCommandDispatcherProtocol
и:
AcquisitionRuntimeEvent
│
▼
AcquisitionRuntimeEventPublisherProtocol
Контракты определяют:
- какие команды допустимы внутри Acquisition Runtime;
- какие события может публиковать Runtime;
- каким образом вызывающий компонент передаёт команду;
- каким образом инфраструктурный результат передаётся потребителям.
Однако контракт сам по себе не выполняет команду.
Например:
ConnectCommand
уже существует как типизированная команда, но в системе пока отсутствует компонент, который обязан:
ConnectCommand
│
▼
WebSocketSessionProtocol.start()
Аналогично:
DisconnectCommand
│
▼
WebSocketSessionProtocol.stop()
Для команд управления подписками и отправки сообщений также пока отсутствует единая точка исполнения.
Таким образом между публичным Runtime Protocol Layer и низкоуровневыми WebSocket-контрактами остаётся незаполненная сервисная граница.
Предпосылки
Настоящий Build опирается на архитектурные решения, принятые в предыдущих этапах.
Build 059.10
Сформированы базовые контракты WebSocket Runtime:
WebSocketTransportProtocol
WebSocketSessionProtocol
WebSocketSubscriptionManagerProtocol
Данные контракты описывают отдельные инфраструктурные возможности, но не объединяют их в единый сервис исполнения команд.
Build 059.11–059.12
Сформированы транспортные команды и события Runtime.
Появились типизированные модели:
ConnectCommand
DisconnectCommand
SubscribeCommand
UnsubscribeCommand
SendTextCommand
SendBinaryCommand
а также соответствующие инфраструктурные события.
Build 060.18
Создана подсистема Trade Stream Consistency.
Она полностью независима от Runtime Transport и принимает только канонические объекты Trade.
Build 060.19
Создана подсистема Trade Recovery.
Recovery остаётся stateless и зависит только от:
TradeStreamConsistencyProtocol
Build 060.20.1
Владение состоянием Trade Stream перенесено в:
TradeStreamStateStore
Runtime Transport окончательно перестал рассматриваться как владелец состояния предметной области.
Build 060.21
Сформирован интеграционный контракт Runtime Protocol Layer.
Добавлены:
AcquisitionRuntimeCommand
AcquisitionRuntimeEvent
AcquisitionRuntimeCommandDispatcherProtocol
AcquisitionRuntimeEventPublisherProtocol
Имена были намеренно ограничены контекстом Acquisition, чтобы исключить конфликт с существующей общесистемной подсистемой:
src/runtime_events/
Build 060.21 определил границы взаимодействия, но сознательно не создавал производственную реализацию.
Проблема
В текущем состоянии каждая Runtime-команда является только неизменяемым объектом данных.
Например:
ConnectCommand()
не содержит логики подключения.
Она только выражает намерение вызывающего компонента.
То же относится к:
DisconnectCommand()
SubscribeCommand(...)
UnsubscribeCommand(...)
SendTextCommand(...)
SendBinaryCommand(...)
Для выполнения команд необходим отдельный сервис, который:
- принимает объект команды;
- определяет его конкретный тип;
- выбирает соответствующую инфраструктурную зависимость;
- выполняет строго определённую операцию;
- не содержит знаний о Trade, Consistency или Recovery.
Без такого компонента Runtime Protocol Layer остаётся декларативным и не может использоваться следующими уровнями системы.
Основная идея Build
Главная идея Build 060.22 заключается в создании тонкого сервиса маршрутизации Runtime-команд.
Новый компонент:
AcquisitionRuntimeService
становится производственной реализацией:
AcquisitionRuntimeCommandDispatcherProtocol
Сервис принимает типизированную команду и делегирует выполнение уже существующим инфраструктурным контрактам.
Концептуально:
AcquisitionRuntimeCommand
│
▼
AcquisitionRuntimeService
│
┌─────┴─────┐
▼ ▼
WebSocket Subscription
Session Manager
Сервис не реализует WebSocket самостоятельно.
Он не содержит сетевого клиента.
Он не создаёт транспорт.
Он только координирует существующие зависимости.
Цель Build
Build обязан создать единственную производственную точку исполнения типизированных Acquisition Runtime Commands.
После завершения этапа система должна обеспечивать:
- приём всех команд из
AcquisitionRuntimeCommand; - детерминированную маршрутизацию каждой команды;
- делегирование lifecycle-команд WebSocket Session;
- делегирование управления подписками Subscription Manager;
- делегирование отправки транспортных сообщений соответствующей зависимости;
- соответствие
AcquisitionRuntimeCommandDispatcherProtocol; - независимое unit-тестирование без реального сетевого соединения.
Build не должен изменять поведение существующих Runtime Commands и Runtime Events.
Что НЕ входит в Scope Build
Настоящий Build сознательно не реализует:
- интеграцию Runtime Service в корневой Acquisition Service;
- интеграцию Runtime Service с Trades Feed;
- Composition Root;
- реальный WebSocket client;
- Runtime Supervisor;
- Reconnect Controller;
- Heartbeat;
- Scheduler;
- автоматическое восстановление подписок;
- автоматический REST Recovery после reconnect;
- обработку входящих Trade-сообщений;
- публикацию Canonical Trade Stream;
- регистрацию Runtime Service в Registry;
- общесистемную шину событий;
- конкурентную очередь команд;
- фоновый worker;
- сохранение Runtime-состояния между перезапусками.
Эти задачи относятся к Build 060.23–060.25 либо к отдельным последующим этапам.
Build 060.22 отвечает только за синхронную с точки зрения порядка и асинхронную с точки зрения Python API диспетчеризацию одной команды за один вызов.
Архитектурные принципы
При реализации Build 060.22 используются следующие обязательные принципы.
1. Unique File Naming
Во всём репозитории Dzentra запрещено создание нескольких файлов с одинаковым именем независимо от расположения в каталогах.
Единственное разрешённое исключение:
__init__.py
Поэтому в Build не создаются файлы:
service.py
protocol.py
exceptions.py
test_service.py
Утверждённые уникальные имена:
acquisition_runtime_service.py
acquisition_runtime_service_protocol.py
test_acquisition_runtime_service.py
Имя каждого файла должно однозначно определять его назначение без учёта родительского каталога.
2. Protocol Before Implementation
Публичный контракт Runtime Service фиксируется отдельно от его реализации.
Внешние потребители в последующих Build должны зависеть от:
AcquisitionRuntimeServiceProtocol
а не от конкретного класса:
AcquisitionRuntimeService
3. Thin Application Service
Runtime Service является тонким координатором.
Он:
- принимает команду;
- определяет её тип;
- делегирует операцию;
- завершает вызов.
Сервис не переносит в себя логику нижележащих компонентов.
4. No Domain Knowledge
Runtime Service не должен импортировать:
Trade
TradeStreamConsistencyProtocol
TradeRecoveryProtocol
TradesFeed
TradeStreamStateStore
Он работает исключительно с транспортными командами и инфраструктурными контрактами.
5. Dependency Injection
Все зависимости передаются в конструктор.
Сервис не создаёт самостоятельно:
- WebSocket Session;
- Transport;
- Subscription Manager;
- Publisher;
- Adapter.
Создание графа зависимостей относится к Composition Root и не входит в Scope Build.
6. One Command — One Deterministic Route
Каждый тип команды должен иметь ровно один допустимый маршрут исполнения.
Команда не может быть обработана несколькими зависимостями одновременно, если это отдельно не определено спецификацией.
7. No Premature Orchestration
Runtime Service не является Supervisor.
Он не запускает фоновые задачи.
Не выполняет retries.
Не планирует reconnect.
Не контролирует heartbeat.
Перечисленные обязанности появятся в специализированных компонентах позднее.
8. Existing Contracts Remain Stable
Build не изменяет публичные модели:
runtime_commands.py
runtime_events.py
transport_messages.py
websocket_protocol.py
Если реализация обнаружит недостаточность существующего контракта, изменение должно быть отдельно согласовано до внесения в код.
Acquisition Runtime Service
Назначение
AcquisitionRuntimeService является первым исполняемым компонентом Runtime Layer.
Его единственная задача — выполнение типизированных инфраструктурных Runtime-команд.
Сервис не содержит собственной бизнес-логики.
Он выполняет только маршрутизацию команд к уже существующим инфраструктурным зависимостям.
Концептуально:
Runtime Command
│
▼
AcquisitionRuntimeService
│
├──────────────► WebSocketSession
│
├──────────────► WebSocketTransport
│
└──────────────► SubscriptionManager
Таким образом сервис становится единственной производственной точкой исполнения Runtime-команд.
Почему появляется отдельный Service
Во время проектирования были рассмотрены несколько вариантов.
Вариант №1
Каждый вызывающий компонент самостоятельно выполняет команды.
Например:
TradesFeed
↓
if ConnectCommand
↓
session.start()
Отклонён.
Причины:
- дублирование логики;
- нарушение принципа единственной ответственности;
- отсутствие единой точки маршрутизации.
Вариант №2
Перенести выполнение команд внутрь WebSocketSession.
Например:
session.dispatch(command)
Отклонён.
Причины:
Session начинает знать о:
- Subscription;
- Transport;
- Runtime Commands.
Тем самым нарушается разделение обязанностей.
Вариант №3
Создать специализированный Runtime Service.
Принят.
Именно Runtime Service становится единственным компонентом, который понимает соответствие между:
Runtime Command
↓
Infrastructure Action
Граница ответственности
Runtime Service отвечает исключительно за выполнение Runtime-команд.
Он НЕ отвечает за:
- сетевой протокол;
- обработку сообщений;
- Consistency;
- Recovery;
- Trades Feed;
- управление жизненным циклом приложения;
- Supervisor;
- Scheduler;
- Heartbeat;
- Reconnect.
Все перечисленные обязанности принадлежат другим Build.
AcquisitionRuntimeServiceProtocol
Назначение
Build 060.22 вводит отдельный публичный контракт сервисного уровня.
AcquisitionRuntimeServiceProtocol
Он описывает единственную публичную возможность Runtime Service.
dispatch(...)
Все последующие Build должны зависеть именно от данного Protocol.
Конкретная реализация может изменяться без влияния на потребителей.
Почему вводится отдельный Protocol
На первый взгляд может показаться, что уже существует:
AcquisitionRuntimeCommandDispatcherProtocol
Однако данный Protocol описывает инфраструктурную возможность диспетчеризации команд.
Он не определяет существование самостоятельного сервисного компонента.
Build 060.22 впервые вводит именно сервисный уровень Runtime.
Поэтому появляется отдельный сервисный контракт.
Это позволяет в будущем заменить реализацию Runtime Service без изменения Composition Root и вышестоящих компонентов.
Публичный API
Сервис предоставляет только один публичный метод.
async def dispatch(
command: AcquisitionRuntimeCommand,
) -> None
Других публичных методов Build 060.22 не вводит.
В частности отсутствуют:
connect()
disconnect()
subscribe()
unsubscribe()
send()
Все операции выражаются исключительно через типизированные команды.
Почему отсутствуют отдельные методы
Во время проектирования рассматривалась следующая модель.
runtime.connect()
runtime.disconnect()
runtime.subscribe(...)
Она была отклонена.
Причина:
типизированные Runtime Commands уже являются официальным языком взаимодействия Runtime Layer.
Создание второго API привело бы к существованию двух независимых способов выполнения одной и той же операции.
Архитектура должна содержать единственный публичный механизм.
Зависимости Runtime Service
Сервис получает все зависимости через Dependency Injection.
Минимальный набор зависимостей выглядит следующим образом.
AcquisitionRuntimeService
│
├────────► WebSocketSessionProtocol
│
├────────► WebSocketTransportProtocol
│
├────────► WebSocketSubscriptionManagerProtocol
│
└────────► AcquisitionRuntimeEventPublisherProtocol
Никакие другие зависимости Build 060.22 не предусматривает.
Почему внедряется Event Publisher
Хотя Build 060.22 ещё не реализует полноценную публикацию Runtime-событий, сервис уже принимает зависимость:
AcquisitionRuntimeEventPublisherProtocol
Это принципиальное архитектурное решение.
Причины:
- исключается изменение конструктора в следующих Build;
- Runtime Service сразу проектируется как источник инфраструктурных событий;
- интеграция публикации становится локальным изменением без перестройки графа зависимостей.
На данном этапе Publisher допускается не использовать.
Почему внедряется WebSocketTransportProtocol
Большинство текущих операций выполняются через:
WebSocketSessionProtocol
Однако команды:
SendTextCommand
SendBinaryCommand
относятся к транспортному уровню.
Поэтому Runtime Service сразу получает доступ к Transport.
Это предотвращает последующее изменение конструктора.
Маршрутизация команд
Главной обязанностью Runtime Service является детерминированная маршрутизация.
Каждый тип команды имеет ровно один маршрут исполнения.
ConnectCommand
ConnectCommand
↓
WebSocketSession.start()
После успешного завершения управление возвращается вызывающему компоненту.
Сам Runtime Service не создаёт сетевое соединение.
DisconnectCommand
DisconnectCommand
↓
WebSocketSession.stop()
Сервис не закрывает транспорт напрямую.
Эта ответственность принадлежит Session.
SubscribeCommand
SubscribeCommand
↓
WebSocketSubscriptionManager.subscribe()
Runtime Service не хранит список активных подписок.
Он лишь делегирует выполнение соответствующему компоненту.
UnsubscribeCommand
UnsubscribeCommand
↓
WebSocketSubscriptionManager.unsubscribe()
После завершения операции Runtime Service не изменяет собственного состояния.
SendTextCommand
SendTextCommand
↓
WebSocketTransport.send_text()
Передаваемое сообщение считается полностью сформированным.
Runtime Service не сериализует полезную нагрузку повторно.
SendBinaryCommand
SendBinaryCommand
↓
WebSocketTransport.send_binary()
Команда делегируется транспортному уровню без каких-либо преобразований.
Официальная матрица маршрутизации
| Runtime Command | Исполнитель |
|---|---|
| ConnectCommand | WebSocketSessionProtocol |
| DisconnectCommand | WebSocketSessionProtocol |
| SubscribeCommand | WebSocketSubscriptionManagerProtocol |
| UnsubscribeCommand | WebSocketSubscriptionManagerProtocol |
| SendTextCommand | WebSocketTransportProtocol |
| SendBinaryCommand | WebSocketTransportProtocol |
Данная таблица считается официальной спецификацией Build 060.22.
Любое изменение маршрутов требует отдельного архитектурного решения (ADR).
Детерминированность маршрутизации
Runtime Service рассматривается как детерминированный маршрутизатор.
Для каждого входящего объекта существует единственный допустимый маршрут.
Формально:
Runtime Command
↓
Dispatch Table
↓
Infrastructure Operation
Никакие дополнительные проверки, эвристики или выбор стратегии Build 060.22 не предусматривает.
Обработка неизвестной команды
Build 060.22 не допускает существование неизвестных Runtime-команд.
Если в сервис поступает объект, не входящий в объединение:
AcquisitionRuntimeCommand
это считается внутренней архитектурной ошибкой.
В таком случае сервис обязан немедленно завершить выполнение исключением.
Это гарантирует, что добавление новой команды никогда не останется незамеченным и потребует явного обновления таблицы маршрутизации.
Диаграмма взаимодействия
После реализации Build 060.22 выполнение Runtime-команд приобретает следующий вид.
Caller
│
│ dispatch(command)
▼
AcquisitionRuntimeService
│
├──────────────► WebSocketSessionProtocol
│
├──────────────► WebSocketTransportProtocol
│
├──────────────► WebSocketSubscriptionManagerProtocol
│
└──────────────► AcquisitionRuntimeEventPublisherProtocol
Важно отметить, что Runtime Service остаётся полностью синхронным с точки зрения архитектуры.
Он:
- не создаёт фоновых задач;
- не ставит команды в очередь;
- не выполняет повторные попытки;
- не содержит собственного event loop.
Каждый вызов dispatch() завершается только после завершения соответствующей инфраструктурной операции.
Жизненный цикл Runtime Service
Runtime Service является долгоживущим инфраструктурным сервисом.
Типичный жизненный цикл выглядит следующим образом.
Composition Root
↓
создание зависимостей
↓
создание Runtime Service
↓
передача Runtime Service вызывающим компонентам
↓
многократные вызовы dispatch()
↓
завершение приложения
Сам Runtime Service не требует отдельной инициализации.
Также отсутствуют методы:
start()
stop()
shutdown()
dispose()
Экземпляр полностью готов к работе сразу после создания.
Отношение к состоянию
Runtime Service является stateless-компонентом.
Он не хранит:
- состояние подключения;
- список подписок;
- очередь сообщений;
- информацию о последней выполненной команде;
- счётчики reconnect;
- состояние Recovery;
- состояние Consistency.
Все перечисленные данные принадлежат специализированным компонентам.
Следовательно экземпляр Runtime Service можно считать чистым координатором.
Обработка исключений
Build 060.22 сознательно не вводит собственую иерархию исключений.
Причина проста.
Runtime Service не принимает самостоятельных решений.
Он лишь вызывает инфраструктурные зависимости.
Следовательно любые исключения должны передаваться вызывающему компоненту без изменения.
Например:
Caller
↓
Runtime Service
↓
WebSocket Session
↓
ConnectionError
Исключение должно пройти обратно без обёртки.
Это сохраняет прозрачность поведения системы.
Почему исключения не преобразуются
Во время проектирования рассматривался вариант создания:
RuntimeDispatchError
Он был отклонён.
Причины:
- потеря информации о первичном исключении;
- необходимость лишнего уровня обработки;
- усложнение диагностики;
- нарушение принципа прозрачной инфраструктуры.
Runtime Service не должен скрывать происхождение ошибки.
Влияние на существующую архитектуру
Build 060.22 практически не изменяет существующую систему.
Новый сервис располагается между контрактами Runtime и инфраструктурными зависимостями.
До Build:
Runtime Commands
↓
(нет реализации)
После Build:
Runtime Commands
↓
AcquisitionRuntimeService
↓
Infrastructure
Никакие другие подсистемы не изменяются.
Влияние на Trade Stream Consistency
Подсистема:
Trade Stream Consistency
не получает никаких новых зависимостей.
Она продолжает работать исключительно с:
Trade
и
TradeStreamConsistencyProtocol
Build 060.22 не создаёт прямой связи между Runtime и Consistency.
Влияние на Trade Recovery
Trade Recovery также не изменяется.
Контроллер восстановления по-прежнему зависит только от:
TradeStreamConsistencyProtocol
Runtime Service не импортирует Recovery.
Recovery не импортирует Runtime Service.
Архитектурная независимость сохраняется полностью.
Влияние на Trades Feed
На данном этапе Trades Feed ещё не использует Runtime Service.
Интеграция будет выполнена отдельным Build:
060.23
Это позволяет протестировать Runtime Service изолированно.
План изменения структуры проекта
Build добавляет только два новых файла.
runtime/
├── acquisition_runtime_service.py
└── acquisition_runtime_service_protocol.py
Также добавляется один файл тестов.
tests/
runtime/
└── test_acquisition_runtime_service.py
Другие файлы Runtime Build 060.22 не изменяет.
Почему используется отдельный Protocol
Наличие собственного:
AcquisitionRuntimeServiceProtocol
позволяет в будущем заменить реализацию.
Например:
Simple Runtime Service
↓
Queued Runtime Service
↓
Distributed Runtime Service
Все перечисленные варианты смогут реализовывать один и тот же публичный контракт.
Поэтому вышестоящие компоненты не будут зависеть от конкретного класса.
Стратегия тестирования
Build 060.22 покрывается исключительно Unit Test.
Никакие интеграционные тесты пока не требуются.
Каждая Runtime-команда проверяется отдельно.
Минимальный набор сценариев включает:
- соответствие
AcquisitionRuntimeServiceProtocol; - корректную маршрутизацию
ConnectCommand; - корректную маршрутизацию
DisconnectCommand; - корректную маршрутизацию
SubscribeCommand; - корректную маршрутизацию
UnsubscribeCommand; - корректную маршрутизацию
SendTextCommand; - корректную маршрутизацию
SendBinaryCommand; - отсутствие побочных эффектов;
- прозрачное распространение исключений;
- отсутствие собственного состояния между вызовами.
Все зависимости заменяются простыми Fake-реализациями.
Использование реального WebSocket Build 060.22 запрещает.
ADR (Architecture Decision Record)
ADR-060.22-01
Решение
Создать специализированный сервис:
AcquisitionRuntimeService
реализующий выполнение всех типизированных Runtime-команд.
Статус
Accepted.
ADR-060.22-02
Решение
Ввести отдельный публичный контракт:
AcquisitionRuntimeServiceProtocol
независимо от уже существующего:
AcquisitionRuntimeCommandDispatcherProtocol
Статус
Accepted.
ADR-060.22-03
Решение
Runtime Service остаётся stateless.
Статус
Accepted.
ADR-060.22-04
Решение
Все зависимости передаются исключительно через Dependency Injection.
Статус
Accepted.
ADR-060.22-05
Решение
Не создавать новых файлов с именами:
service.py
protocol.py
test_service.py
Использовать только уникальные имена файлов.
Статус
Accepted.
Definition of Done
Build считается завершённым только при выполнении всех условий.
Обязательно должны существовать:
acquisition_runtime_service_protocol.py;acquisition_runtime_service.py;test_acquisition_runtime_service.py.
Runtime Service обязан:
- реализовывать
AcquisitionRuntimeServiceProtocol; - принимать зависимости через конструктор;
- поддерживать все типы
AcquisitionRuntimeCommand; - выполнять детерминированную маршрутизацию;
- не хранить собственного состояния;
- не создавать зависимости самостоятельно;
- прозрачно распространять исключения.
Все новые Unit Test должны успешно проходить.
Build не должен изменять поведение существующих Runtime Commands, Runtime Events, Trade Recovery и Trade Stream Consistency.
Следующий Build
После завершения Build 060.22 архитектура Runtime впервые получит полноценный исполняемый сервисный слой.
Следующим этапом станет Build 060.23 — Acquisition Integration.
На этом этапе AcquisitionRuntimeService будет интегрирован в подсистему получения рыночных данных и станет использоваться как единая точка исполнения инфраструктурных Runtime-команд.
После Build 060.23 Runtime перестанет существовать только как набор контрактов и сервисов и начнёт участвовать в реальном конвейере получения данных от биржи.