553 lines
19 KiB
Markdown
553 lines
19 KiB
Markdown
# Build 060.24 — Runtime Recovery Architecture
|
||
|
||
**Engineering Migration Report**
|
||
|
||
---
|
||
|
||
## Контроль документа
|
||
|
||
| Свойство | Значение |
|
||
|---|---|
|
||
| Build | 060.24 |
|
||
| Название | Runtime Recovery Architecture |
|
||
| Статус | Completed |
|
||
| Проект | Dzentra |
|
||
| Подсистема | Market Data Acquisition |
|
||
| Компонент | Trade Stream Runtime |
|
||
| Версия документа | 2.0 |
|
||
|
||
## Связанные документы
|
||
|
||
- `build_060_24_architecture.md` — итоговая архитектурная спецификация Build.
|
||
- `build_060_23.md` — Trade Stream Acquisition Integration.
|
||
- `build_060_23_architecture.md` — архитектура интеграции Trade Stream Acquisition.
|
||
- `build_060_22.md` — Runtime Service Integration.
|
||
- `build_060_20_1.md` — Trade Stream State Ownership Alignment.
|
||
- `build_060_19.md` — Trade Recovery.
|
||
- `build_060_18.md` — Trade Stream Consistency.
|
||
|
||
---
|
||
|
||
## 1. Исходное состояние
|
||
|
||
После Build 060.23 проект имел рабочий конвейер обработки одной live-сделки:
|
||
|
||
```text
|
||
WebSocket message
|
||
│
|
||
▼
|
||
TradeStreamMessageAdapterProtocol
|
||
│
|
||
▼
|
||
Canonical Trade
|
||
│
|
||
▼
|
||
TradeStreamConsistencyController
|
||
│
|
||
▼
|
||
Trade | None
|
||
```
|
||
|
||
Также уже существовал REST Recovery Layer:
|
||
|
||
```text
|
||
TradeRecoveryRequest
|
||
│
|
||
▼
|
||
TradeRecoveryController
|
||
│
|
||
▼
|
||
TradeRecoveryNormalizer
|
||
│
|
||
▼
|
||
TradeStreamConsistencyController
|
||
```
|
||
|
||
Эти два конвейера использовали Consistency Layer, но Runtime ещё не
|
||
обладал архитектурой обнаружения потери активности, повторного подключения
|
||
и координации восстановления пропущенного диапазона.
|
||
|
||
## 2. Цель Build
|
||
|
||
Цель Build 060.24 — реализовать внутреннюю Runtime Recovery Architecture:
|
||
|
||
- определить единственный checkpoint Trade Stream;
|
||
- представить и рассчитать допустимые окна восстановления;
|
||
- реализовать изолированные runtime-компоненты reconnect, heartbeat,
|
||
supervisor и scheduler;
|
||
- связать checkpoint, planner и существующий Recovery Controller;
|
||
- построить единый граф зависимостей Live Stream и Recovery;
|
||
- не запускать production lifecycle и не создавать сетевые подключения
|
||
во время композиции.
|
||
|
||
Build создаёт завершённую архитектурную основу восстановления в памяти.
|
||
Её подключение к реальному WebSocket lifecycle, bootstrap приложения
|
||
и production-конфигурации относится к Build 060.25.
|
||
|
||
---
|
||
|
||
## 3. Выполненный roadmap
|
||
|
||
| Подэтап | Название | Фактическая реализация |
|
||
|---|---|---|
|
||
| 060.24.1 | RuntimeCheckpoint | `TradeStreamState.last_trade` |
|
||
| 060.24.2 | RuntimeSessionState | `RuntimeSupervisorState` и локальные состояния runtime-компонентов |
|
||
| 060.24.3 | RecoveryPlanner | `TradeRecoveryWindowPlanner` |
|
||
| 060.24.4 | RecoveryWindowCalculator | алгоритм `TradeRecoveryWindowPlanner.build_windows()` |
|
||
| 060.24.5 | ReconnectCoordinator | `ReconnectCoordinator` |
|
||
| 060.24.6 | Heartbeat | `HeartbeatMonitor` |
|
||
| 060.24.7 | Supervisor | `RuntimeSupervisor` |
|
||
| 060.24.8 | Scheduler | `RuntimeScheduler` |
|
||
| 060.24.9.1 | RuntimeRecoveryCoordinator | `RuntimeRecoveryProtocol` и `RuntimeRecoveryCoordinator` |
|
||
| 060.24.9.2 | Runtime Composition | `TradeStreamRuntimeComposition` и factory-функция сборки |
|
||
|
||
`RuntimeSessionState` не введён как отдельная дублирующая модель.
|
||
Состояние runtime-сессии выражено через `RuntimeSupervisorState`, а
|
||
локальные детали принадлежат `ReconnectState` и `HeartbeatState`.
|
||
|
||
`RecoveryWindowCalculator` не введён как отдельный класс. Расчёт является
|
||
единственной ответственностью `TradeRecoveryWindowPlanner`, поэтому его
|
||
выделение создало бы искусственный слой без самостоятельного состояния
|
||
или политики.
|
||
|
||
---
|
||
|
||
## 4. Реализованные компоненты
|
||
|
||
### 4.1. RuntimeCheckpoint
|
||
|
||
`TradeStreamState` хранит:
|
||
|
||
```text
|
||
last_trade_id
|
||
last_trade
|
||
recent trade window
|
||
```
|
||
|
||
Checkpoint — это `last_trade`, то есть последняя каноническая сделка,
|
||
принятая Consistency Layer.
|
||
|
||
Он обновляется только после успешного `accept()`. Полный дубликат,
|
||
конфликтующий дубликат, нарушение порядка или неожиданный symbol
|
||
не переводят checkpoint вперёд.
|
||
|
||
Основной инвариант:
|
||
|
||
```text
|
||
last_trade is None
|
||
⇔
|
||
last_trade_id is None
|
||
```
|
||
|
||
Если checkpoint существует:
|
||
|
||
```text
|
||
last_trade.trade_id == last_trade_id
|
||
last_trade.symbol == TradeStreamState.symbol
|
||
```
|
||
|
||
Checkpoint находится только в оперативной памяти. Его долговременное
|
||
хранение относится к Build 060.28.
|
||
|
||
### 4.2. TradeRecoveryWindow
|
||
|
||
`TradeRecoveryWindow` — immutable value object с `slots`:
|
||
|
||
```text
|
||
symbol
|
||
start_time
|
||
end_time
|
||
```
|
||
|
||
Модель проверяет типы и запрещает:
|
||
|
||
- пустой symbol;
|
||
- отрицательные временные границы;
|
||
- `start_time > end_time`;
|
||
- использование `bool` вместо целочисленного timestamp.
|
||
|
||
Window не выполняет Recovery и не содержит transport-, retry- или
|
||
exchange-состояние.
|
||
|
||
### 4.3. TradeRecoveryWindowPlanner
|
||
|
||
Planner принимает полный диапазон и возвращает:
|
||
|
||
```text
|
||
tuple[TradeRecoveryWindow, ...]
|
||
```
|
||
|
||
Каждое окно удовлетворяет ограничению существующего
|
||
`TradeRecoveryRequest`:
|
||
|
||
```text
|
||
end_time - start_time < 3_600_000 ms
|
||
```
|
||
|
||
Значение по умолчанию:
|
||
|
||
```text
|
||
3_599_999 ms
|
||
```
|
||
|
||
Соседние окна имеют общую границу:
|
||
|
||
```text
|
||
previous.end_time == next.start_time
|
||
```
|
||
|
||
Возможный повтор сделки на границе устраняется общим Consistency Layer.
|
||
При равных `start_time` и `end_time` Planner возвращает пустой tuple.
|
||
|
||
Planner:
|
||
|
||
- не выполняет REST-запросы;
|
||
- не создаёт `TradeRecoveryRequest`;
|
||
- не читает системное время;
|
||
- не зависит от WebSocket и Runtime lifecycle.
|
||
|
||
### 4.4. ReconnectCoordinator
|
||
|
||
`ReconnectCoordinator` выполняет одну попытку reconnect:
|
||
|
||
```text
|
||
ReconnectStartedEvent
|
||
│
|
||
▼
|
||
ConnectCommand
|
||
│
|
||
▼
|
||
restore_subscriptions()
|
||
│
|
||
▼
|
||
ReconnectCompletedEvent
|
||
```
|
||
|
||
При ошибке публикуется `ReconnectFailedEvent`, состояние переходит
|
||
в `FAILED`, а исходное исключение распространяется вызывающему коду.
|
||
|
||
Coordinator хранит номер попытки и состояние:
|
||
|
||
```text
|
||
DISCONNECTED
|
||
CONNECTING
|
||
RESTORING_SUBSCRIPTIONS
|
||
CONNECTED
|
||
FAILED
|
||
```
|
||
|
||
Retry loop и backoff в этот компонент не входят.
|
||
|
||
### 4.5. HeartbeatMonitor
|
||
|
||
Heartbeat является пассивным монитором:
|
||
|
||
- использует monotonic clock;
|
||
- хранит время последней активности;
|
||
- определяет timeout детерминированным вызовом `check_timeout()`;
|
||
- публикует один `HeartbeatTimeoutEvent` на один период отсутствия
|
||
активности;
|
||
- возвращается в monitoring после новой активности.
|
||
|
||
Heartbeat не создаёт фоновых задач, не выполняет reconnect и не управляет
|
||
WebSocket.
|
||
|
||
### 4.6. RuntimeSupervisor
|
||
|
||
Supervisor владеет состоянием runtime-сессии:
|
||
|
||
```text
|
||
STOPPED
|
||
RUNNING
|
||
RECONNECTING
|
||
FAILED
|
||
```
|
||
|
||
Он:
|
||
|
||
- запускает и останавливает Heartbeat;
|
||
- принимает уведомления об активности;
|
||
- запускает одну reconnect-попытку после подтверждённого timeout;
|
||
- предотвращает параллельный reconnect;
|
||
- после успешного reconnect начинает новый heartbeat-период;
|
||
- сохраняет `FAILED` и распространяет ошибку reconnect.
|
||
|
||
Supervisor не выполняет периодический цикл, recovery, retry или backoff.
|
||
|
||
### 4.7. RuntimeScheduler
|
||
|
||
Scheduler отвечает только за периодичность:
|
||
|
||
```text
|
||
HeartbeatMonitor.check_timeout()
|
||
│
|
||
├── False → следующий интервал
|
||
│
|
||
└── True → RuntimeSupervisor.handle_heartbeat_timeout()
|
||
```
|
||
|
||
Цикл запускается явным `await RuntimeScheduler.start()`. Scheduler
|
||
не создаёт `asyncio.Task` самостоятельно. `stop()` запрашивает безопасное
|
||
завершение на ближайшей управляемой границе.
|
||
|
||
### 4.8. RuntimeRecoveryCoordinator
|
||
|
||
Coordinator соединяет существующие слои:
|
||
|
||
```text
|
||
TradeStreamStateStore
|
||
│
|
||
▼
|
||
TradeStreamState.last_trade
|
||
│
|
||
▼
|
||
executed_at → Unix milliseconds
|
||
│
|
||
▼
|
||
TradeRecoveryWindowPlanner
|
||
│
|
||
▼
|
||
TradeRecoveryRequest[]
|
||
│
|
||
▼
|
||
TradeRecoveryController
|
||
│
|
||
▼
|
||
aggregated TradeRecoveryResult
|
||
```
|
||
|
||
Свойства реализации:
|
||
|
||
- использует существующее состояние и не создаёт новый
|
||
`TradeStreamState`;
|
||
- принимает `recovery_end_time` от вызывающего runtime-компонента;
|
||
- преобразует только timezone-aware `datetime`;
|
||
- не использует float при переводе времени в Unix milliseconds;
|
||
- выполняет окна последовательно;
|
||
- объединяет восстановленные сделки в один результат;
|
||
- не скрывает ошибки Store, Planner или Recovery Controller.
|
||
|
||
При отсутствии состояния или checkpoint возвращается пустой
|
||
`TradeRecoveryResult`; обе его границы равны `recovery_end_time`.
|
||
|
||
### 4.9. Runtime Composition
|
||
|
||
Factory:
|
||
|
||
```text
|
||
build_trade_stream_runtime_composition(...)
|
||
```
|
||
|
||
создаёт один граф зависимостей и возвращает immutable
|
||
`TradeStreamRuntimeComposition`.
|
||
|
||
Ключевой identity-инвариант:
|
||
|
||
```text
|
||
Live Stream ───┐
|
||
├── one TradeStreamConsistencyController
|
||
Recovery ──────┘
|
||
│
|
||
▼
|
||
one TradeStreamStateStore
|
||
```
|
||
|
||
Благодаря этому live-сделки и восстановленные сделки проходят одинаковые
|
||
правила согласованности и используют один checkpoint.
|
||
|
||
Внешние WebSocket-зависимости передаются через Protocol-контракты.
|
||
Создание Composition:
|
||
|
||
- не читает Settings;
|
||
- не создаёт production transport;
|
||
- не подключается к сети;
|
||
- не запускает Scheduler или Supervisor;
|
||
- не создаёт фоновые задачи;
|
||
- не выполняет Recovery.
|
||
|
||
---
|
||
|
||
## 5. Архитектурные границы
|
||
|
||
После Build действуют следующие границы.
|
||
|
||
| Компонент | Владеет | Не владеет |
|
||
|---|---|---|
|
||
| `TradeStreamState` | checkpoint и deduplication window | recovery orchestration |
|
||
| `TradeRecoveryWindowPlanner` | расчёт окон | REST и runtime lifecycle |
|
||
| `ReconnectCoordinator` | одна reconnect-попытка | retry, backoff и recovery |
|
||
| `HeartbeatMonitor` | контроль активности | scheduling и reconnect |
|
||
| `RuntimeSupervisor` | состояние сессии и реакция на timeout | периодический цикл и recovery |
|
||
| `RuntimeScheduler` | периодический вызов проверок | reconnect-логика и heartbeat-вычисления |
|
||
| `RuntimeRecoveryCoordinator` | recovery orchestration одного symbol | WebSocket lifecycle |
|
||
| Composition root | создание и связывание объектов | запуск production lifecycle |
|
||
|
||
Ни один из компонентов Build не импортирует bootstrap приложения,
|
||
торговую логику или Telegram-инфраструктуру.
|
||
|
||
---
|
||
|
||
## 6. Состав изменений
|
||
|
||
### Production
|
||
|
||
```text
|
||
app/src/market_data/acquisition/
|
||
├── consistency/
|
||
│ └── trade_stream_state.py
|
||
├── recovery/
|
||
│ ├── trade_recovery_window.py
|
||
│ └── trade_recovery_window_planner.py
|
||
├── runtime/
|
||
│ ├── heartbeat.py
|
||
│ ├── reconnect.py
|
||
│ ├── runtime_recovery_coordinator.py
|
||
│ ├── runtime_recovery_protocol.py
|
||
│ ├── scheduler.py
|
||
│ └── supervisor.py
|
||
└── trade_stream_runtime_composition.py
|
||
```
|
||
|
||
### Tests
|
||
|
||
```text
|
||
app/tests/unit/market_data/acquisition/
|
||
├── consistency/
|
||
│ └── test_trade_stream_state.py
|
||
├── recovery/
|
||
│ ├── test_trade_recovery_window.py
|
||
│ └── test_trade_recovery_window_planner.py
|
||
├── runtime/
|
||
│ ├── test_heartbeat_monitor.py
|
||
│ ├── test_reconnect_coordinator.py
|
||
│ ├── test_runtime_recovery_coordinator.py
|
||
│ ├── test_runtime_scheduler.py
|
||
│ └── test_runtime_supervisor.py
|
||
└── test_trade_stream_runtime_composition.py
|
||
```
|
||
|
||
### Documentation
|
||
|
||
```text
|
||
docs/migrations/
|
||
├── build_060_24.md
|
||
└── build_060_24_architecture.md
|
||
```
|
||
|
||
Изменение `.gitignore`, присутствующее в рабочем дереве, не является
|
||
частью Build 060.24.
|
||
|
||
---
|
||
|
||
## 7. Тестирование
|
||
|
||
Непосредственно связанные с Build девять тестовых модулей содержат
|
||
230 тестов.
|
||
|
||
| Область | Тестов |
|
||
|---|---:|
|
||
| Trade Stream State | 24 |
|
||
| Recovery Window | 20 |
|
||
| Recovery Window Planner | 36 |
|
||
| Reconnect Coordinator | 15 |
|
||
| Heartbeat Monitor | 28 |
|
||
| Runtime Supervisor | 25 |
|
||
| Runtime Scheduler | 30 |
|
||
| Runtime Recovery Coordinator | 27 |
|
||
| Runtime Composition | 25 |
|
||
| **Итого** | **230** |
|
||
|
||
Контрольный результат:
|
||
|
||
```text
|
||
230 passed
|
||
```
|
||
|
||
Полная регрессия проекта:
|
||
|
||
```text
|
||
1620 passed
|
||
0 failed
|
||
```
|
||
|
||
Тесты являются unit- и composition-тестами. Реальные сценарии
|
||
WebSocket disconnect, повторной подписки, reconnect → recovery
|
||
и стресс-тестирование относятся к Build 060.25–060.26.
|
||
|
||
---
|
||
|
||
## 8. Обратная совместимость
|
||
|
||
Build сохраняет существующие публичные контракты:
|
||
|
||
- каноническая модель `Trade` не изменена;
|
||
- Recovery Controller и Recovery Request не изменены;
|
||
- live acquisition продолжает использовать Consistency Protocol;
|
||
- transport protocols остаются внешними зависимостями Runtime;
|
||
- `main.py` и bootstrap не изменены.
|
||
|
||
Расширение `TradeStreamState` добавляет checkpoint, не изменяя контракт
|
||
`accept()`.
|
||
|
||
---
|
||
|
||
## 9. Что не входит в Build
|
||
|
||
Build 060.24 не включает:
|
||
|
||
- создание production WebSocket transport/session;
|
||
- подключение Composition к `main.py` или bootstrap;
|
||
- запуск scheduler-задачи;
|
||
- передачу live-активности в Supervisor;
|
||
- выбор symbol и `recovery_end_time` для production recovery;
|
||
- автоматическую цепочку reconnect → recovery;
|
||
- переключение между Recovery и Live Stream;
|
||
- retry/backoff policy;
|
||
- постоянное хранение checkpoint;
|
||
- интеграционные reconnect/recovery и stress tests.
|
||
|
||
Эти ограничения означают, что формулировка
|
||
«Trade Stream самостоятельно переживает обрыв WebSocket» на данном этапе
|
||
является архитектурной целью, а не активированным production-поведением.
|
||
|
||
Production Runtime Integration выполняется в Build 060.25.
|
||
|
||
---
|
||
|
||
## 10. Definition of Done
|
||
|
||
- [x] Checkpoint принадлежит `TradeStreamState`.
|
||
- [x] Checkpoint обновляется только принятой сделкой.
|
||
- [x] Реализована immutable-модель Recovery Window.
|
||
- [x] Реализован расчёт одного или нескольких допустимых окон.
|
||
- [x] Реализована одна reconnect-попытка и восстановление подписок.
|
||
- [x] Реализован пассивный Heartbeat.
|
||
- [x] Реализовано состояние и поведение Runtime Supervisor.
|
||
- [x] Реализован периодический Runtime Scheduler.
|
||
- [x] Реализован Runtime Recovery Coordinator.
|
||
- [x] Реализована Runtime Composition с общим Consistency Layer.
|
||
- [x] Создание Composition не имеет lifecycle- и network-side effects.
|
||
- [x] Целевые тесты проходят.
|
||
- [x] Полная регрессия проходит.
|
||
- [ ] Production lifecycle подключён к приложению — Build 060.25.
|
||
- [ ] End-to-end reconnect/recovery проверен — Build 060.25–060.26.
|
||
|
||
---
|
||
|
||
## 11. Итог
|
||
|
||
Build 060.24 завершает внутреннюю Runtime Recovery Architecture.
|
||
|
||
Проект располагает:
|
||
|
||
- надёжным checkpoint в Consistency Layer;
|
||
- детерминированным планированием Recovery Window;
|
||
- изолированными компонентами reconnect, heartbeat, supervisor
|
||
и scheduler;
|
||
- координатором последовательного восстановления;
|
||
- явным composition root с единым состоянием для live и recovery.
|
||
|
||
Следующий этап не должен заново проектировать эти компоненты.
|
||
Задача Build 060.25 — подключить готовый граф к production WebSocket
|
||
lifecycle и определить end-to-end порядок запуска, reconnect и recovery.
|