1638 lines
49 KiB
Markdown
1638 lines
49 KiB
Markdown
# Build 060.25 — Production Runtime Integration Architecture
|
||
|
||
**Статус:** Completed
|
||
|
||
**Build:** 060.25
|
||
|
||
**Подсистема:** Market Data Acquisition / Trade Stream Runtime
|
||
|
||
**Дата начала:** 2026-07-30
|
||
|
||
**Дата завершения:** 2026-07-31
|
||
|
||
**Версия документа:** 1.0
|
||
|
||
---
|
||
|
||
## 1. Назначение
|
||
|
||
Документ фиксирует архитектуру Production Runtime Integration для
|
||
Trade Stream.
|
||
|
||
Build 060.24 создал внутренний граф Runtime Recovery, но намеренно:
|
||
|
||
- не создавал реальное WebSocket-соединение;
|
||
- не запускал receive loop;
|
||
- не управлял asyncio-задачами;
|
||
- не связывал reconnect с Trade Recovery;
|
||
- не подключал Runtime к bootstrap приложения.
|
||
|
||
Build 060.25 превратил этот граф в управляемый production
|
||
lifecycle без нарушения границ Transport, Runtime, Acquisition,
|
||
Consistency и Recovery.
|
||
|
||
Документ является живой архитектурной спецификацией.
|
||
|
||
Он разделяет:
|
||
|
||
- принятые и реализованные решения 060.25.0–060.25.8;
|
||
- end-to-end, reconnect и stress validation Build 060.26;
|
||
- persistent storage и startup recovery Build 060.27–060.28.
|
||
|
||
Итоговый migration report зафиксирован в `build_060_25.md`.
|
||
|
||
---
|
||
|
||
## 2. Статус подэтапов
|
||
|
||
| Подэтап | Название | Статус |
|
||
|---|---|---|
|
||
| 060.25.0 | Static Contract Cleanup | Accepted |
|
||
| 060.25.1 | Dzengi WebSocket Transport | Accepted |
|
||
| 060.25.2 | WebSocket Session and Subscription Manager | Accepted |
|
||
| 060.25.3 | Async Runtime Event Publisher | Accepted |
|
||
| 060.25.4 | Trade Stream Production Runtime | Accepted |
|
||
| 060.25.5 | Reconnect and Recovery Integration | Accepted |
|
||
| 060.25.6 | Heartbeat and Scheduler Integration | Accepted |
|
||
| 060.25.7 | Settings, Bootstrap and Graceful Shutdown | Accepted |
|
||
| 060.25.8 | Targeted Runtime Verification | Accepted |
|
||
|
||
Статус `Accepted` означает:
|
||
|
||
- реализация прошла целевые тесты;
|
||
- архитектурные findings устранены;
|
||
- пользователь принял результат подэтапа;
|
||
- файлы ещё не зафиксированы отдельным Build commit.
|
||
|
||
---
|
||
|
||
## 3. Исходная архитектура
|
||
|
||
Build 060.24 предоставил:
|
||
|
||
```text
|
||
TradeStreamRuntimeComposition
|
||
├── TradeStreamStateStore
|
||
├── TradeStreamConsistencyController
|
||
├── TradeRecoveryController
|
||
├── TradeRecoveryWindowPlanner
|
||
├── RuntimeRecoveryCoordinator
|
||
├── AcquisitionRuntimeService
|
||
├── TradeStreamAcquisitionService
|
||
├── ReconnectCoordinator
|
||
├── HeartbeatMonitor
|
||
├── RuntimeSupervisor
|
||
└── RuntimeScheduler
|
||
```
|
||
|
||
Composition создаёт единые экземпляры stateful-компонентов, но не
|
||
запускает их.
|
||
|
||
Внешними зависимостями оставались:
|
||
|
||
```text
|
||
WebSocketTransportProtocol
|
||
WebSocketSessionProtocol
|
||
WebSocketSubscriptionManagerProtocol
|
||
AcquisitionRuntimeEventPublisherProtocol
|
||
TradeStreamMessageAdapterProtocol
|
||
DzengiTradesDocumentSource
|
||
```
|
||
|
||
До Build 060.25 production-реализации первых четырёх WebSocket Runtime
|
||
контрактов отсутствовали.
|
||
|
||
---
|
||
|
||
## 4. Целевая модель
|
||
|
||
```text
|
||
APPLICATION BOOTSTRAP
|
||
│
|
||
▼
|
||
Trade Stream Production Runtime
|
||
│
|
||
┌───────────────────────┼───────────────────────┐
|
||
│ │ │
|
||
▼ ▼ ▼
|
||
WebSocket lifecycle Runtime supervision Recovery gate
|
||
│ │ │
|
||
▼ ▼ ▼
|
||
Dzengi Transport Heartbeat / Scheduler Runtime Recovery
|
||
│ │ │
|
||
▼ ▼ ▼
|
||
WebSocket Session Runtime Supervisor REST Recovery
|
||
│ │ │
|
||
▼ ▼ │
|
||
Subscription Manager ───► Reconnect Coordinator │
|
||
│ │
|
||
▼ │
|
||
receive loop │
|
||
│ │
|
||
▼ ▼
|
||
TradeStreamAcquisitionService ─► TradeStreamConsistencyController
|
||
│
|
||
▼
|
||
TradeStreamStateStore
|
||
```
|
||
|
||
Production Runtime является владельцем выполнения, но не владельцем
|
||
бизнес-состояния сделок.
|
||
|
||
---
|
||
|
||
## 5. Архитектурные границы
|
||
|
||
### 5.1. Dzengi WebSocket Transport
|
||
|
||
Transport отвечает только за:
|
||
|
||
- нормализацию WebSocket URL;
|
||
- открытие и закрытие соединения;
|
||
- отправку `str | bytes`;
|
||
- получение `str | bytes`;
|
||
- transport timeouts и ping/pong параметры библиотеки;
|
||
- преобразование инфраструктурных ошибок в WebSocket transport errors.
|
||
|
||
Transport не:
|
||
|
||
- декодирует JSON;
|
||
- различает ACK и market events;
|
||
- строит subscription payload;
|
||
- изменяет Trade Stream state;
|
||
- выполняет reconnect policy;
|
||
- выполняет Recovery.
|
||
|
||
### 5.2. WebSocket Session
|
||
|
||
Session отвечает за:
|
||
|
||
- идемпотентные `start()` и `stop()`;
|
||
- сериализацию lifecycle через `asyncio.Lock`;
|
||
- согласование локального состояния с Transport;
|
||
- повторный `start()` после фактического закрытия Transport.
|
||
|
||
Session не должна превращать повторный `start()` в неявный force
|
||
reconnect. Принудительная замена соединения является явной операцией
|
||
ReconnectCoordinator.
|
||
|
||
### 5.3. Subscription Manager
|
||
|
||
Subscription Manager отвечает за:
|
||
|
||
- desired subscription registry;
|
||
- active state текущего соединения;
|
||
- идемпотентную подписку;
|
||
- последовательное восстановление подписок;
|
||
- очистку runtime-состояния;
|
||
- явную политику unsupported unsubscribe.
|
||
|
||
Manager работает с:
|
||
|
||
```text
|
||
TransportTextMessage
|
||
TransportBinaryMessage
|
||
```
|
||
|
||
Он не анализирует содержимое payload и не знает о Trade, Quote, Candle
|
||
или конкретной JSON destination.
|
||
|
||
### 5.4. Production Runtime
|
||
|
||
Production Runtime должен отвечать за:
|
||
|
||
- запуск и остановку полного графа;
|
||
- владение asyncio-задачами;
|
||
- единственный receive loop;
|
||
- JSON decoding;
|
||
- отделение control/ACK сообщений от market events;
|
||
- передачу market documents в Acquisition Service;
|
||
- координацию reconnect и recovery;
|
||
- graceful cancellation и shutdown.
|
||
|
||
Production Runtime не должен дублировать:
|
||
|
||
- Trade consistency;
|
||
- subscription registry;
|
||
- heartbeat calculations;
|
||
- recovery window planning;
|
||
- WebSocket transport logic.
|
||
|
||
---
|
||
|
||
## 6. Build 060.25.0 — Static Contract Cleanup
|
||
|
||
### 6.1. Исходная проблема
|
||
|
||
Тестовый helper `create_coordinator()` принимал:
|
||
|
||
```text
|
||
RecordingWindowPlanner | None
|
||
```
|
||
|
||
Негативный тест передавал:
|
||
|
||
```text
|
||
BrokenPlanner(TradeRecoveryWindowPlanner)
|
||
```
|
||
|
||
Runtime-поведение было корректным, но Pylance обнаруживал несовместимый
|
||
тип аргумента.
|
||
|
||
### 6.2. Принятое решение
|
||
|
||
Тестовая иерархия согласована с реальной:
|
||
|
||
```text
|
||
TradeRecoveryWindowPlanner
|
||
│
|
||
▼
|
||
RecordingWindowPlanner
|
||
│
|
||
▼
|
||
BrokenPlanner
|
||
```
|
||
|
||
Удалён локальный `type: ignore[arg-type]`.
|
||
|
||
Production-контракт RuntimeRecoveryCoordinator не изменён.
|
||
|
||
---
|
||
|
||
## 7. Build 060.25.1 — Dzengi WebSocket Transport
|
||
|
||
### 7.1. Реализация
|
||
|
||
Добавлен:
|
||
|
||
```text
|
||
DzengiWebSocketTransport
|
||
```
|
||
|
||
Transport структурно соответствует:
|
||
|
||
```text
|
||
WebSocketTransportProtocol
|
||
```
|
||
|
||
Прямое наследование от Protocol не используется, чтобы сохранить
|
||
рабочий `__slots__`.
|
||
|
||
### 7.2. URL policy
|
||
|
||
Поддерживаемые исходные схемы:
|
||
|
||
```text
|
||
http → ws
|
||
https → wss
|
||
ws → ws
|
||
wss → wss
|
||
```
|
||
|
||
URL нормализуется до endpoint:
|
||
|
||
```text
|
||
/connect
|
||
```
|
||
|
||
Примеры:
|
||
|
||
```text
|
||
https://api-adapter.dzengi.com
|
||
↓
|
||
wss://api-adapter.dzengi.com/connect
|
||
```
|
||
|
||
```text
|
||
http://localhost:8080/
|
||
↓
|
||
ws://localhost:8080/connect
|
||
```
|
||
|
||
Пустые, относительные, неподдерживаемые и fragment URL отклоняются до
|
||
сетевого вызова.
|
||
|
||
### 7.3. Connection options
|
||
|
||
Через constructor внедряются:
|
||
|
||
```text
|
||
headers
|
||
open_timeout
|
||
ping_interval
|
||
ping_timeout
|
||
close_timeout
|
||
connector
|
||
```
|
||
|
||
Инъекция connector позволяет тестировать Transport без реальной сети.
|
||
|
||
Transport использует WebSocket subprotocol:
|
||
|
||
```text
|
||
json
|
||
```
|
||
|
||
### 7.4. Lifecycle
|
||
|
||
```text
|
||
DISCONNECTED
|
||
│ connect()
|
||
▼
|
||
OPEN
|
||
│ disconnect()
|
||
▼
|
||
DISCONNECTED
|
||
```
|
||
|
||
Повторные `connect()` и `disconnect()` идемпотентны.
|
||
|
||
Lifecycle сериализован отдельным `asyncio.Lock`.
|
||
|
||
Transport считает соединение активным только если:
|
||
|
||
```text
|
||
connection is not None
|
||
and connection.state is State.OPEN
|
||
```
|
||
|
||
### 7.5. Error policy
|
||
|
||
Введены:
|
||
|
||
```text
|
||
WebSocketTransportError
|
||
WebSocketTransportNotConnectedError
|
||
```
|
||
|
||
Connect, disconnect, send и receive errors оборачиваются с сохранением
|
||
исходной ошибки в `__cause__`.
|
||
|
||
Попытка send или receive без открытого соединения завершается
|
||
`WebSocketTransportNotConnectedError`.
|
||
|
||
`asyncio.CancelledError` не поглощается обработчиками `Exception`.
|
||
|
||
---
|
||
|
||
## 8. Build 060.25.2 — WebSocket Session
|
||
|
||
### 8.1. Реализация
|
||
|
||
Добавлен:
|
||
|
||
```text
|
||
WebSocketSession
|
||
```
|
||
|
||
Session структурно соответствует:
|
||
|
||
```text
|
||
WebSocketSessionProtocol
|
||
```
|
||
|
||
### 8.2. Идемпотентность
|
||
|
||
```text
|
||
start()
|
||
├── Session и Transport connected → no-op
|
||
└── disconnected → transport.connect()
|
||
```
|
||
|
||
```text
|
||
stop()
|
||
├── Session и Transport disconnected → no-op
|
||
└── active → transport.disconnect()
|
||
```
|
||
|
||
Параллельные lifecycle-вызовы сериализуются Session lock.
|
||
|
||
После ошибки `start()` Session остаётся disconnected.
|
||
|
||
После ошибки `stop()` локальный Session state гарантированно
|
||
сбрасывается в `finally`.
|
||
|
||
### 8.3. Remote disconnect
|
||
|
||
Session не доверяет только локальному `_started`.
|
||
|
||
Состояние:
|
||
|
||
```text
|
||
is_connected =
|
||
_started
|
||
and transport.is_connected
|
||
```
|
||
|
||
Если Transport обнаружил закрытие соединения, следующий `start()`
|
||
действительно открывает новое соединение.
|
||
|
||
---
|
||
|
||
## 9. Build 060.25.2 — Subscription State
|
||
|
||
### 9.1. Два уровня состояния
|
||
|
||
Subscription Manager разделяет:
|
||
|
||
```text
|
||
desired subscriptions
|
||
```
|
||
|
||
и:
|
||
|
||
```text
|
||
active subscription keys
|
||
```
|
||
|
||
Desired registry описывает требуемое состояние Runtime и переживает
|
||
ошибку текущей сетевой попытки.
|
||
|
||
Active keys относятся только к текущему WebSocket connection.
|
||
|
||
### 9.2. Subscribe
|
||
|
||
```text
|
||
validate key and message
|
||
│
|
||
▼
|
||
store desired subscription
|
||
│
|
||
▼
|
||
transport.send()
|
||
│
|
||
├── success → mark active
|
||
└── failure → keep pending desired subscription
|
||
```
|
||
|
||
Повторный вызов для active key является no-op.
|
||
|
||
Повторный вызов для pending key выполняет новую отправку и может
|
||
обновить сохранённое transport message.
|
||
|
||
### 9.3. Restore
|
||
|
||
После нового соединения:
|
||
|
||
```text
|
||
clear active keys
|
||
│
|
||
▼
|
||
for each desired subscription in registration order:
|
||
transport.send()
|
||
│
|
||
▼
|
||
mark key active
|
||
```
|
||
|
||
Ошибка восстановления не удаляет desired registry.
|
||
|
||
### 9.4. Unsubscribe
|
||
|
||
Публичный Dzengi endpoint не поддерживает надёжный
|
||
`trades.unsubscribe`.
|
||
|
||
Поэтому production-default:
|
||
|
||
```text
|
||
supports_unsubscribe = False
|
||
```
|
||
|
||
Попытка unsubscribe завершается:
|
||
|
||
```text
|
||
WebSocketUnsubscribeNotSupportedError
|
||
```
|
||
|
||
и не изменяет registry.
|
||
|
||
Generic supported branch сохраняется для будущих провайдеров.
|
||
|
||
### 9.5. Clear
|
||
|
||
`clear_subscriptions()` очищает desired и active state без отправки
|
||
сетевого сообщения.
|
||
|
||
Это локальная lifecycle-операция, а не имитация exchange unsubscribe.
|
||
|
||
---
|
||
|
||
## 10. Корректирующее решение Reconnect
|
||
|
||
### 10.1. Обнаруженный риск
|
||
|
||
Первоначальная реализация Build 060.24 выполняла:
|
||
|
||
```text
|
||
ConnectCommand
|
||
│
|
||
▼
|
||
restore_subscriptions()
|
||
```
|
||
|
||
Новая идемпотентная Session показала скрытую проблему.
|
||
|
||
При heartbeat timeout WebSocket мог формально оставаться `OPEN`.
|
||
В таком состоянии `Session.start()` являлся no-op, и подписки повторно
|
||
отправлялись в старое соединение.
|
||
|
||
### 10.2. Принятое решение
|
||
|
||
Одна reconnect-попытка теперь выполняет:
|
||
|
||
```text
|
||
ReconnectStartedEvent
|
||
│
|
||
▼
|
||
DisconnectCommand
|
||
│
|
||
▼
|
||
ConnectCommand
|
||
│
|
||
▼
|
||
restore_subscriptions()
|
||
│
|
||
▼
|
||
ReconnectCompletedEvent
|
||
```
|
||
|
||
Это гарантирует замену старого connection даже при формальном
|
||
`State.OPEN`.
|
||
|
||
Registry подписок не очищается при disconnect, поэтому desired state
|
||
доступен для восстановления.
|
||
|
||
### 10.3. Failure policy
|
||
|
||
Ошибка disconnect, connect или restore:
|
||
|
||
```text
|
||
state = FAILED
|
||
│
|
||
▼
|
||
ReconnectFailedEvent
|
||
│
|
||
▼
|
||
raise original exception
|
||
```
|
||
|
||
Retry loop и backoff по-прежнему находятся вне ReconnectCoordinator.
|
||
|
||
---
|
||
|
||
## 11. Concurrency invariants
|
||
|
||
### 11.1. Локальные locks
|
||
|
||
```text
|
||
DzengiWebSocketTransport._lifecycle_lock
|
||
```
|
||
|
||
защищает connect/disconnect.
|
||
|
||
```text
|
||
WebSocketSession._lifecycle_lock
|
||
```
|
||
|
||
защищает Session start/stop.
|
||
|
||
```text
|
||
WebSocketSubscriptionManager._lock
|
||
```
|
||
|
||
защищает subscribe, unsubscribe, restore и clear.
|
||
|
||
### 11.2. Lock ordering
|
||
|
||
Допустимый порядок:
|
||
|
||
```text
|
||
Session lock
|
||
│
|
||
▼
|
||
Transport lifecycle lock
|
||
```
|
||
|
||
```text
|
||
Subscription Manager lock
|
||
│
|
||
▼
|
||
Transport send
|
||
```
|
||
|
||
Transport не вызывает Session или Subscription Manager обратно.
|
||
Обратного lock ordering нет.
|
||
|
||
### 11.3. Cross-component coordination
|
||
|
||
Локальные locks не заменяют общий lifecycle gate.
|
||
|
||
Production Runtime обязан исключить гонку:
|
||
|
||
```text
|
||
live message processing
|
||
X
|
||
reconnect / recovery state mutation
|
||
```
|
||
|
||
Этот gate относится к 060.25.4–060.25.5.
|
||
|
||
---
|
||
|
||
## 12. Build 060.25.3 — Async Runtime Event Publisher
|
||
|
||
**Статус:** Accepted
|
||
|
||
Реализованы:
|
||
|
||
```text
|
||
AcquisitionRuntimeEventConsumerProtocol
|
||
AcquisitionRuntimeEventPublisher
|
||
AcquisitionRuntimeEventLoggingConsumer
|
||
```
|
||
|
||
`AcquisitionRuntimeEventPublisher` является production-реализацией:
|
||
|
||
```text
|
||
AcquisitionRuntimeEventPublisherProtocol
|
||
```
|
||
|
||
### 12.1. Delivery model
|
||
|
||
Publisher:
|
||
|
||
- принимает immutable-набор Consumer через constructor injection;
|
||
- последовательно await-ит Consumer в порядке регистрации;
|
||
- сериализует конкурентные `publish()` одним `asyncio.Lock`;
|
||
- возвращается только после завершения fan-out;
|
||
- не создаёт `asyncio.Task`;
|
||
- не хранит event queue;
|
||
- не имеет собственного `start()` или `stop()`.
|
||
|
||
Рекурсивная публикация через тот же Publisher отклоняется до входа
|
||
в lock. Для определения прямой и child-task reentrancy используется
|
||
`ContextVar`.
|
||
|
||
### 12.2. Consumer failure policy
|
||
|
||
Обычная ошибка одного Consumer:
|
||
|
||
- не прерывает доставку остальным Consumer;
|
||
- не прерывает Heartbeat или Reconnect lifecycle;
|
||
- фиксируется только безопасными метаданными:
|
||
`event type`, `consumer type`, `error type`;
|
||
- не добавляет exception message, traceback или `repr(event)` в лог.
|
||
|
||
Ошибка logging handler также не выходит из Publisher.
|
||
|
||
`CancelledError` и другие `BaseException` не перехватываются.
|
||
Publisher освобождает lock и reentrancy context в `finally`, поэтому
|
||
может использоваться после отменённого вызова.
|
||
|
||
### 12.3. Logging Consumer
|
||
|
||
`AcquisitionRuntimeEventLoggingConsumer` использует стандартный
|
||
Python logging.
|
||
|
||
Lifecycle и failure events получают соответствующие уровни
|
||
`INFO`, `WARNING` и `ERROR`.
|
||
|
||
Для `MessageReceivedEvent` и `MessageSentEvent` на уровне `DEBUG`
|
||
фиксируются только:
|
||
|
||
- тип transport message;
|
||
- размер payload.
|
||
|
||
Содержимое payload не журналируется.
|
||
|
||
### 12.4. Legacy Runtime Events
|
||
|
||
Существующий `src/runtime_events/publisher.py` не изменён.
|
||
|
||
Новый Publisher не зависит от:
|
||
|
||
- `NotificationService`;
|
||
- Telegram;
|
||
- `JournalService`;
|
||
- legacy `RuntimeEvent`;
|
||
- market models.
|
||
|
||
Преобразование Acquisition Runtime Event в legacy Runtime Event
|
||
в настоящий Build не добавлено.
|
||
|
||
---
|
||
|
||
## 13. Build 060.25.4 — Trade Stream Production Runtime
|
||
|
||
**Статус:** Accepted
|
||
|
||
Реализован один высокоуровневый владелец lifecycle:
|
||
|
||
TradeStreamProductionRuntime
|
||
|
||
### 13.1. Задачи
|
||
|
||
Production Runtime владеет:
|
||
|
||
- startup sequence;
|
||
- receive task;
|
||
- lifecycle gate;
|
||
- cancellation;
|
||
- graceful shutdown;
|
||
- terminal error propagation.
|
||
|
||
Startup, Scheduler и receive выполняются как явно сохранённые owned
|
||
tasks.
|
||
Внешний bootstrap остаётся владельцем корневой coroutine `run()`.
|
||
|
||
Production Runtime синхронно закрепляет Scheduler за собой до первого
|
||
await сетевого startup. Поэтому другой lifecycle owner не может
|
||
запустить тот же Scheduler во время подключения или подписки.
|
||
|
||
### 13.2. Receive loop
|
||
|
||
Существует ровно один consumer:
|
||
|
||
```text
|
||
transport.receive()
|
||
│
|
||
▼
|
||
decode JSON
|
||
│
|
||
├── ACK / control → runtime control handling
|
||
├── market document → TradeStreamAcquisitionService
|
||
└── invalid document → explicit error policy
|
||
```
|
||
|
||
ACK и control messages нельзя передавать в Unified Market Adapter как
|
||
неизвестный market event.
|
||
|
||
Production Runtime создаёт correlation ID Trade subscription и
|
||
передаёт его через Acquisition Service. Provider-specific handler:
|
||
|
||
- принимает только совпадающий correlation ID;
|
||
- подтверждает успешный ACK;
|
||
- идемпотентно принимает повторный успешный ACK;
|
||
- делает отрицательный ACK terminal error;
|
||
- отклоняет неизвестный correlation ID и malformed control message.
|
||
|
||
### 13.3. Task ownership
|
||
|
||
Production Runtime создаёт, хранит, отменяет и await-ит собственные
|
||
startup, Scheduler и receive tasks.
|
||
|
||
`stop()` может отменить незавершившиеся:
|
||
|
||
- Session startup;
|
||
- публикацию `ConnectedEvent`;
|
||
- отправку Trade subscription.
|
||
|
||
Ни Transport, ни Session, ни Heartbeat не создают скрытые background
|
||
tasks на уровне приложения.
|
||
|
||
---
|
||
|
||
## 14. Build 060.25.5 — Reconnect and Recovery
|
||
|
||
**Статус:** Accepted
|
||
|
||
### 14.1. Основная последовательность
|
||
|
||
```text
|
||
connection failure or confirmed liveness timeout
|
||
│
|
||
▼
|
||
acquire single reconnect/recovery gate
|
||
│
|
||
▼
|
||
pause live message processing
|
||
│
|
||
▼
|
||
disconnect old connection
|
||
│
|
||
▼
|
||
connect new connection
|
||
│
|
||
▼
|
||
restore desired subscriptions
|
||
│
|
||
▼
|
||
capture recovery_end_time
|
||
│
|
||
▼
|
||
run RuntimeRecoveryCoordinator
|
||
│
|
||
▼
|
||
resume buffered live messages
|
||
```
|
||
|
||
### 14.2. Recovery execution
|
||
|
||
`RuntimeRecoveryCoordinator.recover()` является синхронным и использует
|
||
блокирующий REST.
|
||
|
||
В asyncio Runtime он должен выполняться:
|
||
|
||
```text
|
||
await asyncio.to_thread(...)
|
||
```
|
||
|
||
либо через отдельный async adapter с эквивалентной семантикой.
|
||
|
||
### 14.3. Shared state
|
||
|
||
Live и Recovery используют один:
|
||
|
||
```text
|
||
TradeStreamStateStore
|
||
TradeStreamConsistencyController
|
||
```
|
||
|
||
Поэтому live processing должен быть приостановлен на время Recovery.
|
||
|
||
Одновременное изменение state из event loop и recovery thread
|
||
запрещено.
|
||
|
||
### 14.4. Boundary policy
|
||
|
||
После восстановления подписки новые WebSocket frames могут накапливаться
|
||
в transport buffer.
|
||
|
||
Recovery обрабатывается первым, затем buffered live frames.
|
||
|
||
Повтор на общей временной границе устраняется существующим Consistency
|
||
Layer.
|
||
|
||
### 14.5. Single-flight и поколения connection
|
||
|
||
Добавлен:
|
||
|
||
```text
|
||
RuntimeReconnectRecoveryCoordinator
|
||
```
|
||
|
||
Он объединяет базовый `ReconnectCoordinator` и
|
||
`RuntimeRecoveryCoordinator` в одну single-flight операцию.
|
||
|
||
Одновременные запросы reconnect присоединяются к уже выполняющейся
|
||
операции. Ошибка старого connection generation не запускает повторный
|
||
reconnect после того, как её поколение уже было обработано.
|
||
|
||
### 14.6. Cancellation и ошибки Recovery
|
||
|
||
Recovery запускается через `asyncio.to_thread()` при закрытом live gate.
|
||
|
||
Если владеющая asyncio-задача отменена, Runtime продолжает ждать
|
||
фактического завершения Recovery worker. Повторные запросы отмены не
|
||
могут преждевременно открыть gate и оставить worker изменяющим
|
||
Consistency state в фоне.
|
||
|
||
После ошибки reconnect или Recovery gate переходит в failed state.
|
||
Накопленные live messages не передаются в Consistency Layer. Failed
|
||
state очищается только при начале нового полного Production Runtime
|
||
lifecycle.
|
||
|
||
### 14.7. Composition identity
|
||
|
||
Composition создаёт один экземпляр live gate и передаёт один
|
||
`RuntimeReconnectRecoveryCoordinator` в `RuntimeSupervisor`.
|
||
|
||
Production Runtime проверяет:
|
||
|
||
- identity общего live gate;
|
||
- равенство нормализованного набора symbols для Live и Recovery.
|
||
|
||
Несогласованный dependency graph отклоняется до запуска WebSocket
|
||
lifecycle.
|
||
|
||
---
|
||
|
||
## 15. Build 060.25.6 — Heartbeat and Scheduler
|
||
|
||
**Статус:** Accepted
|
||
|
||
### 15.1. Liveness
|
||
|
||
Отсутствие сделок не означает потерю соединения.
|
||
|
||
Для тихого symbol допустима длительная пауза между market events.
|
||
|
||
Поэтому reconnect нельзя запускать только по условию:
|
||
|
||
```text
|
||
no Trade messages
|
||
```
|
||
|
||
Liveness подтверждается двумя transport-level источниками:
|
||
|
||
- успешным WebSocket Ping/Pong;
|
||
- успешно полученным WebSocket message независимо от его market/control
|
||
назначения.
|
||
|
||
### 15.2. Реализованная граница
|
||
|
||
Реализован отдельный контракт:
|
||
|
||
```text
|
||
RuntimeLivenessProbeProtocol
|
||
│
|
||
▼
|
||
WebSocket ping/pong implementation
|
||
│
|
||
├── success → supervisor.notify_activity()
|
||
└── failure → reconnect path
|
||
```
|
||
|
||
`DzengiWebSocketTransport.probe()`:
|
||
|
||
- возвращает `True` только после соответствующего Pong;
|
||
- возвращает `False`, если connection отсутствует, закрыт или Pong не
|
||
получен вовремя;
|
||
- не перехватывает cancellation;
|
||
- распространяет неожиданную ошибку открытого connection как
|
||
`WebSocketTransportError`.
|
||
|
||
Ожидание Pong всегда имеет конечную границу `probe_timeout`.
|
||
|
||
Если отдельное значение не передано, оно берётся из конечного
|
||
`ping_timeout`. При отключённом `ping_timeout` применяется безопасное
|
||
значение 20 секунд. Неположительное и бесконечное значение запрещено.
|
||
|
||
### 15.3. Scheduler
|
||
|
||
Scheduler продолжает владеть только временем вызовов.
|
||
|
||
Каждая итерация:
|
||
|
||
1. выполняет transport probe;
|
||
2. при успешном Pong уведомляет Supervisor об активности;
|
||
3. проверяет Heartbeat timeout;
|
||
4. передаёт подтверждённый timeout Supervisor.
|
||
|
||
Он не должен:
|
||
|
||
- отправлять WebSocket payload самостоятельно;
|
||
- изменять subscription registry;
|
||
- выполнять Trade Recovery;
|
||
- владеть receive loop.
|
||
|
||
### 15.4. Reconnect generation
|
||
|
||
Supervisor связывает каждый Heartbeat monitoring period с generation
|
||
текущего connection.
|
||
|
||
Heartbeat timeout передаётся в
|
||
`reconnect_after_transport_failure(observed_generation=...)`.
|
||
|
||
Поэтому timeout и receive error одного поколения:
|
||
|
||
- присоединяются к одной активной reconnect/recovery operation;
|
||
- используют результат уже завершённой operation;
|
||
- не запускают повторный reconnect после восстановления того же
|
||
connection generation.
|
||
|
||
### 15.5. Task ownership и shutdown
|
||
|
||
Production Runtime выполняет `Scheduler.claim(owner)` до первого
|
||
сетевого await и освобождает claim только после завершения owned task.
|
||
|
||
Это исключает запуск Scheduler вторым владельцем во время startup.
|
||
|
||
Порядок остановки:
|
||
|
||
```text
|
||
Scheduler stop / cancel / await
|
||
↓
|
||
Supervisor stop
|
||
↓
|
||
receive task cancel / await
|
||
↓
|
||
Session stop
|
||
```
|
||
|
||
Scheduler-led reconnect/recovery достигает безопасной границы до
|
||
остановки Supervisor. Receive-led operation безопасно завершается при
|
||
последующем cancel/await receive task и до остановки Session.
|
||
|
||
---
|
||
|
||
## 16. Build 060.25.7 — Settings and Bootstrap
|
||
|
||
**Статус:** Accepted
|
||
|
||
### 16.1. Settings
|
||
|
||
Добавлена отдельная неизменяемая группа:
|
||
|
||
```text
|
||
TradeStreamSettings
|
||
├── enabled
|
||
├── websocket_url
|
||
├── symbols
|
||
├── open_timeout_seconds
|
||
├── probe_timeout_seconds
|
||
├── close_timeout_seconds
|
||
├── heartbeat_timeout_seconds
|
||
├── scheduler_interval_seconds
|
||
└── recovery_window_ms
|
||
```
|
||
|
||
Production Trade Stream должен иметь отдельный feature flag с безопасным
|
||
значением по умолчанию.
|
||
|
||
Общий `EXCHANGE_ENABLED` не должен неявно запускать новый Runtime без
|
||
явной настройки.
|
||
|
||
Используются переменные:
|
||
|
||
```text
|
||
TRADE_STREAM_ENABLED=false
|
||
TRADE_STREAM_WS_URL
|
||
TRADE_STREAM_SYMBOLS
|
||
TRADE_STREAM_OPEN_TIMEOUT_SECONDS=10
|
||
TRADE_STREAM_PROBE_TIMEOUT_SECONDS=20
|
||
TRADE_STREAM_CLOSE_TIMEOUT_SECONDS=10
|
||
TRADE_STREAM_HEARTBEAT_TIMEOUT_SECONDS=30
|
||
TRADE_STREAM_SCHEDULER_INTERVAL_SECONDS=5
|
||
TRADE_STREAM_RECOVERY_WINDOW_MS=3599999
|
||
```
|
||
|
||
При выключенном flag зависимые Trade Stream значения не проверяются и
|
||
не создают Runtime. При включённом flag обязательны отдельные
|
||
`TRADE_STREAM_WS_URL`, `TRADE_STREAM_SYMBOLS` и `EXCHANGE_BASE_URL` для
|
||
REST Recovery.
|
||
|
||
Fallback с `EXCHANGE_WS_URL` и `DEFAULT_SYMBOL` запрещён.
|
||
`EXCHANGE_BASE_URL` и `EXCHANGE_TIMEOUT_SEC` используются только
|
||
явным REST-клиентом Recovery.
|
||
|
||
Все интервалы должны быть положительными и конечными, recovery window —
|
||
положительным целым числом. Символы очищаются от пробелов,
|
||
дедуплицируются и сортируются.
|
||
|
||
### 16.2. Bootstrap
|
||
|
||
Добавлен отдельный production composition root:
|
||
|
||
```text
|
||
build_trade_stream_production_runtime(settings)
|
||
│
|
||
├── feature flag disabled → None
|
||
│
|
||
└── enabled
|
||
├── DzengiWebSocketTransport
|
||
├── WebSocketSession
|
||
├── WebSocketSubscriptionManager
|
||
├── AcquisitionRuntimeEventPublisher
|
||
├── DzengiTradesDocumentSource
|
||
├── TradeStreamRuntimeComposition
|
||
└── TradeStreamProductionRuntime
|
||
```
|
||
|
||
Factory использует один снимок `Settings`. Создание графа:
|
||
|
||
- не открывает WebSocket;
|
||
- не выполняет REST;
|
||
- не создаёт asyncio-задачи;
|
||
- сохраняет identity общих stateful-зависимостей.
|
||
|
||
Встроенный keepalive библиотеки `websockets` отключён:
|
||
|
||
```text
|
||
ping_interval=None
|
||
ping_timeout=None
|
||
```
|
||
|
||
Единственным владельцем активного Ping/Pong является Runtime Scheduler
|
||
через конечный `probe_timeout`.
|
||
|
||
Корневой `ApplicationComposition` содержит Telegram Bot, Dispatcher и
|
||
опциональный Trade Stream Runtime. `run_application()` создаёт и
|
||
контролирует обе root tasks.
|
||
|
||
Ошибка или неожиданное нормальное завершение включённого Trade Stream
|
||
фатальны для всего процесса. Ошибка Telegram polling симметрично
|
||
останавливает Trade Stream. Исходная ошибка сохраняет приоритет над
|
||
ошибками cleanup.
|
||
|
||
`main.py` не должен содержать детали WebSocket protocol или recovery
|
||
алгоритма.
|
||
|
||
### 16.3. Shutdown
|
||
|
||
Внешний порядок приложения:
|
||
|
||
```text
|
||
stop accepting Telegram work
|
||
│
|
||
▼
|
||
TradeStreamProductionRuntime.stop()
|
||
│
|
||
▼
|
||
await both root tasks
|
||
│
|
||
▼
|
||
close bot session exactly once
|
||
```
|
||
|
||
Внутренний порядок Trade Stream Runtime:
|
||
|
||
```text
|
||
Scheduler stop / cancel / await
|
||
│
|
||
▼
|
||
Supervisor stop
|
||
│
|
||
▼
|
||
receive task cancel / await
|
||
│
|
||
▼
|
||
Session stop
|
||
│
|
||
▼
|
||
clear subscriptions
|
||
```
|
||
|
||
Application Runner передаёт Aiogram параметр
|
||
`close_bot_session=False`, поэтому сессию закрывает ровно один корневой
|
||
владелец.
|
||
|
||
Cancellation не прерывает начатый cleanup: cleanup выполняется в
|
||
отдельной owned task и ожидается через shield. Все созданные root tasks
|
||
отменяются либо завершаются и обязательно await-ятся.
|
||
|
||
---
|
||
|
||
## 17. Build 060.25.8 — Verification
|
||
|
||
**Статус:** Accepted
|
||
|
||
060.25.8 не добавляет production-поведение. Этап подтверждает
|
||
детерминированными in-process тестами, что принятые компоненты работают
|
||
как один production graph.
|
||
|
||
Добавлен управляемый test harness:
|
||
|
||
```text
|
||
Production Trade Stream Factory
|
||
│
|
||
├── real DzengiWebSocketTransport
|
||
├── real WebSocketSession
|
||
├── real WebSocketSubscriptionManager
|
||
├── real Runtime Composition
|
||
└── real TradeStreamProductionRuntime
|
||
│
|
||
├── controlled fake WebSocket connector
|
||
├── controlled fake REST client
|
||
└── fake Telegram boundary
|
||
```
|
||
|
||
Harness не использует реальную сеть, реальные credentials или
|
||
недетерминированные внешние задержки.
|
||
|
||
Подтверждены следующие vertical-slice сценарии:
|
||
|
||
1. выключенный feature flag не создаёт Runtime graph;
|
||
2. production factory выполняет subscribe → ACK → Trade → checkpoint;
|
||
3. ошибка WebSocket connect фатальна и не оставляет задач;
|
||
4. ошибка первой subscription send откатывает partial startup;
|
||
5. reconnect открывает новое connection и восстанавливает subscription
|
||
до запуска REST Recovery;
|
||
6. recovered Trade обрабатывается раньше buffered Live Trade;
|
||
7. ошибка Recovery оставляет gate в failed state и отклоняет buffered
|
||
Trade;
|
||
8. одновременные ошибки Telegram и Trade Stream обрабатываются
|
||
детерминированно;
|
||
9. повторная cancellation не прерывает начатый cleanup;
|
||
10. Bot session закрывается один раз, owned tasks всегда await-ятся.
|
||
|
||
Production-код в 060.25.8 не изменён.
|
||
|
||
В Build 060.26 остаются:
|
||
|
||
- live exchange integration;
|
||
- длительные reconnect scenarios;
|
||
- recovery scenarios с реальными задержками;
|
||
- stress testing;
|
||
- network fault injection;
|
||
- финальная Runtime documentation verification.
|
||
|
||
Unit tests по умолчанию не должны обращаться к внешней сети.
|
||
|
||
---
|
||
|
||
## 18. Реализованные файлы
|
||
|
||
### 18.1. Production code
|
||
|
||
Добавлены:
|
||
|
||
```text
|
||
app/src/market_data/acquisition/adapters/dzengi/
|
||
websocket_control_message_handler.py
|
||
websocket_inbound_message_classifier.py
|
||
websocket_transport.py
|
||
|
||
app/src/market_data/acquisition/runtime/
|
||
acquisition_runtime_event_logging_consumer.py
|
||
acquisition_runtime_event_publisher.py
|
||
live_processing_gate.py
|
||
runtime_liveness_probe.py
|
||
runtime_reconnect_recovery_coordinator.py
|
||
trade_stream_production_runtime.py
|
||
websocket_inbound_message.py
|
||
websocket_session.py
|
||
websocket_subscription_manager.py
|
||
|
||
app/src/bootstrap/
|
||
application.py
|
||
trade_stream_runtime.py
|
||
```
|
||
|
||
Изменены:
|
||
|
||
```text
|
||
app/.env.example
|
||
app/src/bootstrap/app_factory.py
|
||
app/src/core/config.py
|
||
app/src/integrations/exchange/rest_client.py
|
||
app/src/main.py
|
||
app/src/market_data/acquisition/exceptions.py
|
||
app/src/market_data/acquisition/runtime/reconnect.py
|
||
app/src/market_data/acquisition/runtime/scheduler.py
|
||
app/src/market_data/acquisition/runtime/supervisor.py
|
||
app/src/market_data/acquisition/trade_stream_runtime_composition.py
|
||
```
|
||
|
||
### 18.2. Tests
|
||
|
||
Добавлены:
|
||
|
||
```text
|
||
app/tests/unit/market_data/acquisition/adapters/dzengi/
|
||
test_websocket_control_message_handler.py
|
||
test_websocket_inbound_message_classifier.py
|
||
test_websocket_transport.py
|
||
|
||
app/tests/unit/market_data/acquisition/runtime/
|
||
test_acquisition_runtime_event_logging_consumer.py
|
||
test_acquisition_runtime_event_publisher.py
|
||
test_live_processing_gate.py
|
||
test_runtime_reconnect_recovery_coordinator.py
|
||
test_trade_stream_production_runtime.py
|
||
test_websocket_session.py
|
||
test_websocket_subscription_manager.py
|
||
test_websocket_runtime_reconnect_integration.py
|
||
|
||
app/tests/unit/bootstrap/
|
||
test_app_factory.py
|
||
test_application.py
|
||
test_trade_stream_production_integration.py
|
||
test_trade_stream_runtime.py
|
||
|
||
app/tests/unit/core/
|
||
test_config.py
|
||
|
||
app/tests/unit/test_main.py
|
||
```
|
||
|
||
Изменены:
|
||
|
||
```text
|
||
app/tests/unit/market_data/acquisition/runtime/
|
||
test_reconnect_coordinator.py
|
||
test_runtime_recovery_coordinator.py
|
||
test_runtime_scheduler.py
|
||
test_runtime_supervisor.py
|
||
|
||
app/tests/unit/market_data/acquisition/
|
||
test_trade_stream_runtime_composition.py
|
||
```
|
||
|
||
Состав файлов соответствует итоговому состоянию Build 060.25.
|
||
|
||
---
|
||
|
||
## 19. Test evidence
|
||
|
||
После реализации 060.25.8 выполнены:
|
||
|
||
```text
|
||
Settings / Bootstrap tests: 30 passed
|
||
New vertical / lifecycle scenarios: 8 passed
|
||
Expanded Production Runtime target: 445 passed
|
||
Full project regression: 1869 passed
|
||
```
|
||
|
||
Расширенный целевой набор включает:
|
||
|
||
- Trade Stream settings;
|
||
- Application Composition, factory и `main.py`;
|
||
- production factory vertical-slice integration;
|
||
- весь `market_data/acquisition/runtime`;
|
||
- Dzengi WebSocket Transport, classifier и control handler;
|
||
- Trade Stream Runtime Composition.
|
||
|
||
Отдельно проверены:
|
||
|
||
```text
|
||
OPEN connection
|
||
│ reconnect()
|
||
▼
|
||
old connection closed
|
||
│
|
||
▼
|
||
new connection opened
|
||
│
|
||
▼
|
||
subscriptions restored
|
||
```
|
||
|
||
и:
|
||
|
||
```text
|
||
initial subscribe send failure
|
||
│
|
||
▼
|
||
desired subscription remains pending
|
||
│ reconnect()
|
||
▼
|
||
subscription sent through new connection
|
||
```
|
||
|
||
Для Async Runtime Event Publisher отдельно подтверждены:
|
||
|
||
- последовательная доставка;
|
||
- сериализация конкурентных публикаций;
|
||
- отсутствие fire-and-forget;
|
||
- consumer error isolation;
|
||
- отсутствие payload в error log;
|
||
- устойчивость к ошибке logging handler;
|
||
- распространение cancellation;
|
||
- повторное использование после cancellation;
|
||
- защита от прямой и child-task reentrancy;
|
||
- сохранение Heartbeat и Reconnect lifecycle при ошибке Consumer.
|
||
|
||
После read-only review 060.25.8 целевой набор и полная регрессия
|
||
повторены 2026-07-31. Новых findings не обнаружено.
|
||
|
||
---
|
||
|
||
## 20. Принятые архитектурные решения
|
||
|
||
### ADR-060.25-001 — Transport остаётся raw boundary
|
||
|
||
**Статус:** Accepted
|
||
|
||
DzengiWebSocketTransport работает только с `str | bytes` и не содержит
|
||
JSON или market routing.
|
||
|
||
### ADR-060.25-002 — Session start является идемпотентным
|
||
|
||
**Статус:** Accepted
|
||
|
||
Повторный start не заменяет исправное открытое соединение.
|
||
|
||
### ADR-060.25-003 — Reconnect всегда выполняет disconnect
|
||
|
||
**Статус:** Accepted
|
||
|
||
Force reconnect выражен последовательностью Disconnect → Connect, а не
|
||
скрытым поведением Session.start().
|
||
|
||
### ADR-060.25-004 — Subscription registry хранит desired state
|
||
|
||
**Статус:** Accepted
|
||
|
||
Сетевой send failure не удаляет намерение подписаться. Desired и active
|
||
state разделены.
|
||
|
||
### ADR-060.25-005 — Unsupported unsubscribe является явной ошибкой
|
||
|
||
**Статус:** Accepted
|
||
|
||
Dzengi unsubscribe не имитируется локальным успешным результатом.
|
||
|
||
### ADR-060.25-006 — Production Runtime владеет задачами
|
||
|
||
**Статус:** Accepted
|
||
|
||
Внешний bootstrap владеет корневой task `run()`. Production Runtime
|
||
до сетевого startup закрепляет Scheduler за собой, а затем создаёт,
|
||
хранит, отменяет и await-ит собственные startup, Scheduler и receive
|
||
tasks. Claim освобождается после завершения Scheduler task.
|
||
|
||
### ADR-060.25-007 — Recovery выполняется при закрытом live gate
|
||
|
||
**Статус:** Accepted
|
||
|
||
REST Recovery и live message processing не изменяют общий Consistency
|
||
state параллельно. Отмена ожидает фактического завершения Recovery
|
||
worker, а ошибка Recovery блокирует buffered live processing до нового
|
||
Runtime lifecycle.
|
||
|
||
### ADR-060.25-008 — Heartbeat основан на transport liveness
|
||
|
||
**Статус:** Accepted
|
||
|
||
Отсутствие Trade events не является достаточным признаком разрыва.
|
||
Liveness подтверждается конечным Ping/Pong probe либо успешно полученным
|
||
WebSocket message. Timeout и receive error используют одно connection
|
||
generation и одну single-flight reconnect/recovery operation.
|
||
|
||
### ADR-060.25-009 — Runtime Events доставляются inline
|
||
|
||
**Статус:** Accepted
|
||
|
||
Publisher последовательно await-ит фиксированный набор Consumer и не
|
||
создаёт скрытую очередь или background task.
|
||
|
||
### ADR-060.25-010 — Consumer errors не управляют Runtime lifecycle
|
||
|
||
**Статус:** Accepted
|
||
|
||
Обычная ошибка Consumer изолируется, безопасно диагностируется и не
|
||
прерывает доставку остальным Consumer. Cancellation распространяется.
|
||
|
||
### ADR-060.25-011 — Trade Stream включается только отдельным flag
|
||
|
||
**Статус:** Accepted
|
||
|
||
`TRADE_STREAM_ENABLED` по умолчанию выключен. `EXCHANGE_ENABLED` не
|
||
запускает новый Runtime, а legacy WebSocket URL и default symbol не
|
||
используются как fallback.
|
||
|
||
### ADR-060.25-012 — Ошибка включённого Runtime фатальна
|
||
|
||
**Статус:** Accepted
|
||
|
||
Telegram polling и Trade Stream принадлежат одному Application Runner.
|
||
Terminal error любой включённой root task останавливает вторую root
|
||
task, после чего обе задачи ожидаются и сессия Bot закрывается ровно
|
||
один раз.
|
||
|
||
### ADR-060.25-013 — Scheduler является единственным keepalive owner
|
||
|
||
**Статус:** Accepted
|
||
|
||
Автоматический keepalive `websockets` отключён. Ping/Pong выполняется
|
||
только Scheduler через явный transport liveness probe с конечным
|
||
timeout.
|
||
|
||
---
|
||
|
||
## 21. Инварианты Build
|
||
|
||
К завершению 060.25 должны выполняться:
|
||
|
||
```text
|
||
exactly one production receive loop
|
||
```
|
||
|
||
```text
|
||
exactly one scheduler task
|
||
```
|
||
|
||
```text
|
||
at most one reconnect/recovery sequence
|
||
```
|
||
|
||
```text
|
||
reconnect =
|
||
disconnect
|
||
→ connect
|
||
→ restore subscriptions
|
||
```
|
||
|
||
```text
|
||
recovery and live state mutation are not concurrent
|
||
```
|
||
|
||
```text
|
||
desired subscriptions survive transient send failure
|
||
```
|
||
|
||
```text
|
||
quiet market does not cause false reconnect
|
||
```
|
||
|
||
```text
|
||
startup and shutdown are deterministic and awaitable
|
||
```
|
||
|
||
```text
|
||
Composition construction has no network side effects
|
||
```
|
||
|
||
```text
|
||
disabled Trade Stream creates no Runtime graph
|
||
```
|
||
|
||
```text
|
||
application owns and awaits both root tasks
|
||
```
|
||
|
||
```text
|
||
bot session is closed exactly once
|
||
```
|
||
|
||
```text
|
||
runtime event delivery is ordered and awaitable
|
||
```
|
||
|
||
```text
|
||
consumer failure does not interrupt runtime lifecycle
|
||
```
|
||
|
||
---
|
||
|
||
## 22. Не входит в Build
|
||
|
||
Build 060.25 не реализует:
|
||
|
||
- persistent market data storage;
|
||
- persistent checkpoint;
|
||
- startup recovery после перезапуска процесса;
|
||
- historical query API;
|
||
- replay API;
|
||
- analytics API;
|
||
- общий retry framework приложения;
|
||
- production stress certification.
|
||
|
||
Эти задачи относятся к Build 060.26–060.29.
|
||
|
||
---
|
||
|
||
## 23. Критерии завершения
|
||
|
||
Build 060.25 получил статус `Completed`, поскольку:
|
||
|
||
- все этапы 060.25.0–060.25.8 приняты;
|
||
- concrete Runtime dependencies собраны через composition root;
|
||
- Trade Stream запускается и останавливается из bootstrap;
|
||
- receive loop передаёт Trade messages в общий Consistency Layer;
|
||
- ACK/control messages не разрушают market pipeline;
|
||
- reconnect действительно заменяет connection;
|
||
- desired subscriptions восстанавливаются;
|
||
- Recovery выполняется до возобновления live processing;
|
||
- heartbeat не зависит только от частоты сделок;
|
||
- cancellation не оставляет фоновые задачи;
|
||
- целевые тесты и полная регрессия проходят;
|
||
- `git diff --check` не содержит новых ошибок;
|
||
- создан итоговый `build_060_25.md`;
|
||
- настоящий документ обновлён до финального фактического состояния.
|
||
|
||
---
|
||
|
||
## 24. Текущий итог
|
||
|
||
На момент версии 1.0:
|
||
|
||
```text
|
||
060.25.0 Accepted
|
||
060.25.1 Accepted
|
||
060.25.2 Accepted
|
||
060.25.3 Accepted
|
||
060.25.4 Accepted
|
||
060.25.5 Accepted
|
||
060.25.6 Accepted
|
||
060.25.7 Accepted
|
||
060.25.8 Accepted
|
||
```
|
||
|
||
Реализован production-ready фундамент WebSocket lifecycle:
|
||
|
||
- concrete Dzengi Transport;
|
||
- идемпотентная Session;
|
||
- desired/active Subscription Manager;
|
||
- принудительная замена connection во время reconnect;
|
||
- интеграционные unit-сценарии двух критических recovery cases;
|
||
- последовательный Async Runtime Event Publisher;
|
||
- безопасный Logging Consumer;
|
||
- изоляция Consumer errors от Heartbeat и Reconnect lifecycle;
|
||
- единый Trade Stream Production Runtime;
|
||
- отменяемые и awaitable startup/receive tasks;
|
||
- явная маршрутизация и проверка Trade subscription ACK;
|
||
- single-flight reconnect → subscription restore → Recovery;
|
||
- общий live/recovery gate с блокировкой buffered сообщений после
|
||
Recovery failure;
|
||
- безопасное ожидание Recovery worker при повторной cancellation;
|
||
- проверка единого набора Live и Recovery symbols;
|
||
- конечный WebSocket Ping/Pong liveness probe;
|
||
- generation-aware объединение heartbeat и receive reconnect;
|
||
- единственная owned Scheduler task;
|
||
- детерминированная остановка Scheduler → Supervisor → receive loop;
|
||
- отдельный безопасный Trade Stream feature flag;
|
||
- production composition root без сетевых side effects;
|
||
- один Application Runner для Telegram и Trade Stream;
|
||
- фатальность terminal error включённого Trade Stream;
|
||
- явные WebSocket URL и symbols без legacy fallback;
|
||
- отключённый встроенный WebSocket keepalive;
|
||
- детерминированное ожидание root tasks и однократное закрытие Bot
|
||
session.
|
||
|
||
Runtime подключён к bootstrap приложения и прошёл финальную
|
||
детерминированную in-process verification. Отдельный read-only review
|
||
060.25.8 не выявил findings; целевой набор из 445 тестов и полная
|
||
регрессия из 1869 тестов прошли. Build 060.25 завершён.
|