Files
dzentra_bot/docs/migrations/build_060_24_architecture.md

1011 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Build 060.24 — Runtime Recovery Architecture Specification
**Статус:** Accepted
**Build:** 060.24
**Подсистема:** Market Data Acquisition / Trade Stream Runtime
---
## 1. Назначение
Документ фиксирует фактическую архитектуру Build 060.24 после завершения
подэтапов 060.24.1060.24.9.2.
Build создаёт компоненты, необходимые для восстановления Trade Stream
после потери активности, и связывает их в единый граф зависимостей.
Документ отделяет:
- завершённую внутреннюю архитектуру Build 060.24;
- production lifecycle и end-to-end интеграцию Build 060.25;
- интеграционные и стресс-сценарии Build 060.26;
- постоянное хранение Build 060.27060.28.
---
## 2. Архитектурный контекст
### 2.1. Live pipeline
```text
WebSocket Transport
AcquisitionRuntimeService
TradeStreamMessageAdapterProtocol
TradeStreamAcquisitionService
TradeStreamConsistencyController
Canonical Trade | None
```
### 2.2. Recovery pipeline
```text
TradeRecoveryRequest
TradeRecoveryController
DzengiTradesDocumentSource
TradeRecoveryNormalizer
TradeStreamConsistencyController
Canonical Trade[]
```
### 2.3. Проблема до Build
До Build компоненты Live и Recovery существовали отдельно.
Отсутствовали:
- надёжная временная точка начала восстановления;
- разбиение диапазона на допустимые REST-окна;
- контроль потери runtime-активности;
- одна формализованная reconnect-попытка;
- состояние runtime-сессии;
- периодический вызов heartbeat;
- координатор checkpoint → recovery;
- composition root с общими stateful-зависимостями.
---
## 3. Итоговая модель
```text
PRODUCTION INPUTS
WebSocket Session / Transport / Subscriptions / Event Publisher
TradeStreamRuntimeComposition
┌────────────────────┼────────────────────┐
│ │ │
▼ ▼ ▼
LIVE ACQUISITION RUNTIME LIFECYCLE RECOVERY
│ │ │
│ HeartbeatMonitor │
│ │ │
│ RuntimeScheduler │
│ │ │
│ RuntimeSupervisor │
│ │ │
│ ReconnectCoordinator │
│ │
▼ ▼
TradeStreamConsistencyController ◄──── TradeRecoveryController
│ ▲
▼ │
TradeStreamStateStore ──► RuntimeRecoveryCoordinator
│ │
▼ ▼
TradeStreamState TradeRecoveryWindowPlanner
│ │
└──── checkpoint ─────────┘
```
Composition создаёт граф, но не запускает его. Владельцем production
задач и lifecycle будет внешний bootstrap Build 060.25.
---
## 4. Слои и направления зависимостей
Допустимое направление:
```text
Composition
├── Runtime
├── Recovery
├── Consistency
└── Acquisition
Runtime Recovery Coordinator
├── State Store Protocol
├── Window Planner
└── Recovery Protocol
Recovery Controller
└── Consistency Protocol
Acquisition Service
└── Consistency Protocol
```
Запрещённые обратные зависимости:
```text
Consistency ─X─► Runtime
Recovery ─X─► WebSocket lifecycle
Heartbeat ─X─► Reconnect implementation
Scheduler ─X─► Transport
Supervisor ─X─► Recovery Controller
```
Composition размещена в корне `market_data.acquisition`, а не внутри
`runtime`, потому что она знает сразу о нескольких соседних слоях.
---
## 5. Ownership состояния
### 5.1. Trade Stream state
Владелец:
```text
TradeStreamStateStore
TradeStreamState(symbol)
```
Содержит:
- последнюю принятую сделку;
- идентификатор последней принятой сделки;
- ограниченное окно deduplication.
### 5.2. Runtime session state
Отдельный `RuntimeSessionState` не создаётся.
Общее состояние lifecycle принадлежит Supervisor:
```text
RuntimeSupervisorState
├── STOPPED
├── RUNNING
├── RECONNECTING
└── FAILED
```
Локальное состояние операции reconnect принадлежит
`ReconnectCoordinator`:
```text
ReconnectState
├── DISCONNECTED
├── CONNECTING
├── RESTORING_SUBSCRIPTIONS
├── CONNECTED
└── FAILED
```
Состояние контроля активности принадлежит Heartbeat:
```text
HeartbeatState
├── IDLE
├── MONITORING
└── TIMED_OUT
```
Это разделение предотвращает появление общего mutable-объекта,
дублирующего состояние специализированных компонентов.
### 5.3. Scheduler state
Scheduler хранит только:
```text
running: bool
```
Флаг описывает состояние его цикла и не является состоянием
WebSocket-сессии.
---
## 6. RuntimeCheckpoint
### 6.1. Владелец
Checkpoint принадлежит только:
```text
TradeStreamState.last_trade
```
Runtime, Recovery Coordinator и Supervisor могут читать checkpoint,
но не владеют им и не изменяют его.
### 6.2. Причина хранения полного Trade
Полный канонический `Trade` уже содержит согласованные:
```text
symbol
trade_id
executed_at
price
quantity
side
source
```
Отдельные поля checkpoint создавали бы риск рассинхронизации.
### 6.3. Инварианты
```text
last_trade is None
last_trade_id is None
```
```text
last_trade.trade_id == last_trade_id
```
```text
last_trade.symbol == state.symbol
```
Checkpoint изменяется только после принятия сделки. Ошибка или duplicate
не должны частично изменить состояние.
### 6.4. Ограничение
Checkpoint является in-memory. Перезапуск процесса его уничтожает.
Persistent Checkpoint относится к Build 060.28.
---
## 7. Recovery Window и расчёт диапазона
### 7.1. TradeRecoveryWindow
Value object:
```text
TradeRecoveryWindow(
symbol: str,
start_time: int,
end_time: int,
)
```
Свойства:
- immutable;
- slots;
- валиден сразу после создания;
- независим от REST, Runtime и конкретной биржи.
### 7.2. TradeRecoveryWindowPlanner
Planner объединяет роли RecoveryPlanner и RecoveryWindowCalculator:
```text
build_windows(
symbol,
start_time,
end_time,
) -> tuple[TradeRecoveryWindow, ...]
```
Отдельный Calculator не вводится, потому что расчёт окон:
- не имеет собственного состояния;
- не имеет политики за пределами Planner;
- не используется независимо от построения recovery plan.
### 7.3. Временные границы
`start_time` и `end_time` представлены Unix milliseconds.
Допустимый диапазон:
```text
0 <= start_time <= end_time
```
Равные границы означают отсутствие необходимого восстановления:
```text
start_time == end_time
()
```
### 7.4. Размер окна
Ограничение Recovery Request:
```text
end_time - start_time < 3_600_000
```
Максимальное значение Planner:
```text
3_599_999 ms
```
### 7.5. Непрерывность
Planner строит последовательность:
```text
[start, boundary_1]
[boundary_1, boundary_2]
[boundary_2, end]
```
Соседние окна разделяют одну границу. Это исключает временной разрыв.
Повтор сделки на общей границе безопасен, потому что Recovery использует
тот же Consistency Layer.
---
## 8. Reconnect
### 8.1. Ответственность
`ReconnectCoordinator.reconnect()` выполняет ровно одну попытку:
```text
attempt += 1
publish ReconnectStartedEvent
dispatch ConnectCommand
restore_subscriptions()
publish ReconnectCompletedEvent
```
### 8.2. Ошибка
```text
connect or restore error
state = FAILED
publish ReconnectFailedEvent
raise original exception
```
### 8.3. Граница
Reconnect не решает:
- когда запускаться;
- сколько раз повторяться;
- какой backoff использовать;
- требуется ли Trade Recovery;
- когда возобновлять live processing.
---
## 9. Heartbeat
Heartbeat — пассивный детектор отсутствия активности.
```text
start()
MONITORING
├── record_activity() → reset monotonic boundary
└── check_timeout()
├── elapsed < timeout → False
└── elapsed >= timeout
TIMED_OUT
HeartbeatTimeoutEvent
```
Событие публикуется один раз до новой активности или нового `start()`.
Используется `time.monotonic`, а не wall clock. Инъекция clock делает
проверки детерминированными.
Heartbeat:
- не содержит собственного периодического цикла;
- не знает о Supervisor;
- не выполняет reconnect;
- не создаёт задачи.
---
## 10. Runtime Supervisor
Supervisor координирует Heartbeat и Reconnect.
### 10.1. Запуск
```text
STOPPED
│ start()
heartbeat.start()
RUNNING
```
### 10.2. Активность
`notify_activity()` передаётся Heartbeat только в состоянии `RUNNING`.
Активность после остановки или во время reconnect не запускает monitor
неявно.
### 10.3. Timeout
```text
RUNNING
│ confirmed timeout
heartbeat.stop()
RECONNECTING
ReconnectCoordinator.reconnect()
├── success → heartbeat.start() → RUNNING
└── failure → FAILED → raise
```
Проверка исходного состояния предотвращает параллельный reconnect.
### 10.4. Граница
Supervisor не:
- вызывает `HeartbeatMonitor.check_timeout()`;
- выполняет периодический sleep;
- планирует retry;
- выполняет recovery;
- управляет Transport напрямую.
---
## 11. Runtime Scheduler
Scheduler владеет только временем вызовов:
```text
while running:
timed_out = await heartbeat.check_timeout()
if timed_out:
await supervisor.handle_heartbeat_timeout()
await sleep(interval_seconds)
```
Реальная реализация дополнительно:
- не запускает второй цикл при повторном `start()`;
- позволяет выполнить один шаг через `run_once()`;
- прекращает цикл через идемпотентный `stop()`;
- сбрасывает `running` в `finally`;
- распространяет ошибки зависимостей без обёртки;
- позволяет инъецировать sleep-функцию.
Scheduler не создаёт свою `asyncio.Task`. Внешний lifecycle определяет,
где и как await/cancel выполняются.
---
## 12. Runtime Recovery Coordinator
### 12.1. Контракт
```text
recover(
symbol: str,
recovery_end_time: int,
) -> TradeRecoveryResult
```
`RuntimeRecoveryProtocol` является structural protocol. Реализация
не наследует Protocol напрямую, что сохраняет рабочий `__slots__`
и позволяет проверять совместимость через `runtime_checkable`.
### 12.2. Последовательность
```text
state_store.get(symbol)
├── state absent ───────────┐
│ │
▼ │
state.last_trade │
│ │
├── checkpoint absent ──────┤
│ │
▼ ▼
executed_at → Unix ms empty TradeRecoveryResult
window_planner.build_windows()
for each window:
TradeRecoveryRequest
recovery_controller.recover()
aggregate recovered_trades
TradeRecoveryResult
```
### 12.3. Time policy
Начальная граница берётся только из checkpoint.
Конечная граница передаётся извне:
```text
recovery_end_time
```
Coordinator не читает часы самостоятельно. Это оставляет production
policy владельцу lifecycle и обеспечивает детерминированные тесты.
`executed_at` обязан быть timezone-aware. Преобразование в Unix
milliseconds выполняется через UTC epoch и целочисленную арифметику.
### 12.4. Empty recovery
Если state или checkpoint отсутствуют:
```text
requested_start_time = recovery_end_time
requested_end_time = recovery_end_time
recovered_trades = ()
```
Coordinator не создаёт state автоматически, потому что владельцем
жизненного цикла state остаётся Consistency Layer.
### 12.5. Error policy
Ошибки Store, Planner и Recovery Controller распространяются без
преобразования. Runtime lifecycle должен решить, как публиковать ошибку
и какую retry policy применять.
---
## 13. Runtime Composition
### 13.1. Входные зависимости
Factory принимает:
- WebSocket session;
- transport;
- subscription manager;
- runtime event publisher;
- message adapter;
- recovery document source;
- heartbeat timeout;
- scheduler interval;
- необязательные window size, clock и sleep.
### 13.2. Создаваемые объекты
```text
TradeStreamStateStore
TradeStreamConsistencyController
TradeRecoveryController
TradeRecoveryWindowPlanner
RuntimeRecoveryCoordinator
AcquisitionRuntimeService
TradeStreamAcquisitionService
ReconnectCoordinator
HeartbeatMonitor
RuntimeSupervisor
RuntimeScheduler
```
### 13.3. Identity graph
```text
TradeStreamAcquisitionService
└──────────────┐
TradeStreamConsistencyController
TradeRecoveryController
RuntimeRecoveryCoordinator
TradeStreamConsistencyController
TradeStreamStateStore
RuntimeRecoveryCoordinator
```
Один store и один consistency controller являются обязательным
инвариантом композиции.
### 13.4. Side-effect policy
Factory только создаёт объекты.
Она не:
- выполняет connect;
- создаёт asyncio-задачу;
- запускает Scheduler;
- запускает Supervisor;
- читает Settings;
- выполняет Recovery;
- публикует Runtime Events.
Это позволяет безопасно строить граф в тестах и в будущем bootstrap.
### 13.5. Не Service Locator
`TradeStreamRuntimeComposition` — typed assembly result, а не глобальный
registry. Он immutable и явно передаётся владельцу lifecycle.
---
## 14. Протоколы
На границах используются Protocol-контракты:
```text
WebSocketSessionProtocol
WebSocketTransportProtocol
WebSocketSubscriptionManagerProtocol
AcquisitionRuntimeCommandDispatcherProtocol
AcquisitionRuntimeEventPublisherProtocol
HeartbeatMonitorProtocol
ReconnectCoordinatorProtocol
RuntimeSupervisorProtocol
RuntimeSchedulerProtocol
RuntimeRecoveryProtocol
TradeStreamStateStoreProtocol
TradeRecoveryProtocol
```
Протоколы позволяют:
- независимо тестировать компоненты;
- не привязывать runtime-логику к конкретному transport;
- инъецировать clock и sleep;
- сохранить однонаправленные зависимости.
---
## 15. Ошибки и восстановление состояния
| Компонент | Поведение при ошибке |
|---|---|
| Checkpoint | не изменяется при отклонённой сделке |
| Planner | отклоняет некорректный диапазон до построения окон |
| Reconnect | публикует failure event, устанавливает `FAILED`, распространяет ошибку |
| Heartbeat | распространяет ошибку publisher |
| Supervisor | устанавливает `FAILED`, распространяет reconnect error |
| Scheduler | завершает цикл, сбрасывает `running`, распространяет ошибку |
| Recovery Coordinator | не скрывает ошибки Store, Planner и Recovery |
| Composition | ошибка конструктора прерывает сборку без запуска lifecycle |
Build не вводит общий error envelope и retry policy.
---
## 16. Concurrency и lifecycle
Компоненты Build не создают неявных фоновых задач.
Единственный длительный цикл находится в:
```text
await RuntimeScheduler.start()
```
Владелец внешнего lifecycle обязан:
- создать и хранить task Scheduler;
- вызвать `RuntimeSupervisor.start()`;
- передавать live activity через `notify_activity()`;
- остановить Scheduler и Supervisor;
- обработать cancellation;
- определить reconnect/recovery order.
Эти обязанности относятся к Production Runtime Integration.
---
## 17. Архитектурные решения
### ADR-060.24-001 — Checkpoint принадлежит Consistency Layer
**Статус:** Accepted
`TradeStreamState.last_trade` является единственным источником точки
восстановления.
### ADR-060.24-002 — Checkpoint хранит полный Canonical Trade
**Статус:** Accepted
Отдельные timestamp, symbol и trade id не дублируются в Runtime.
### ADR-060.24-003 — RuntimeSessionState распределён по владельцам
**Статус:** Accepted
Общий lifecycle хранится Supervisor, локальные состояния — Heartbeat
и Reconnect. Общий mutable session object не создаётся.
### ADR-060.24-004 — Window Calculator является частью Planner
**Статус:** Accepted
Расчёт окна не имеет независимой ответственности, поэтому отдельный
Calculator не вводится.
### ADR-060.24-005 — Shared boundary устраняется Consistency Layer
**Статус:** Accepted
Окна имеют общую временную границу, а возможные повторы удаляются
централизованной deduplication-логикой.
### ADR-060.24-006 — Heartbeat пассивен
**Статус:** Accepted
Heartbeat не владеет task или scheduler. Он только хранит активность
и проверяет timeout.
### ADR-060.24-007 — Reconnect выполняет одну попытку
**Статус:** Accepted
Retry и backoff остаются внешней policy.
### ADR-060.24-008 — Recovery End Time передаётся извне
**Статус:** Accepted
Coordinator не читает системное время и не принимает production policy.
### ADR-060.24-009 — Runtime Recovery не владеет WebSocket
**Статус:** Accepted
Recovery Coordinator связывает state, planning и REST Recovery,
не выполняя connect, disconnect или subscription restore.
### ADR-060.24-010 — Composition не запускает lifecycle
**Статус:** Accepted
Factory создаёт граф без network-, task- и configuration-side effects.
---
## 18. Проверяемые инварианты
### State
- checkpoint равен последней принятой сделке;
- checkpoint и `last_trade_id` согласованы;
- duplicate и error не изменяют checkpoint.
### Planning
- каждое окно валидно;
- окна не превышают configured maximum;
- окна покрывают полный диапазон без разрывов;
- равные границы дают пустой plan.
### Runtime
- Heartbeat публикует timeout однократно;
- Supervisor не запускает параллельный reconnect;
- Reconnect восстанавливает подписки только после connect;
- Scheduler не создаёт второй loop;
- ошибки не скрываются.
### Recovery
- отсутствие state/checkpoint не создаёт state;
- окна выполняются последовательно;
- результаты агрегируются в исходных границах;
- recovered Trade сохраняют identity;
- timezone и Unix millisecond conversion детерминированы.
### Composition
- live и recovery используют один consistency controller;
- coordinator и consistency controller используют один state store;
- runtime-компоненты получают ровно те зависимости, которые созданы
factory;
- создание графа не вызывает runtime-методы.
---
## 19. Тестовая стратегия
Build проверяется на трёх уровнях.
### Value objects и чистые вычисления
- checkpoint invariants;
- Recovery Window validation;
- Window Planner boundaries.
### Изолированные runtime-компоненты
- protocol compatibility;
- state transitions;
- event order;
- async interaction;
- error propagation;
- injected clock и sleep.
### Composition
- типы компонентов;
- identity общего Store и Consistency Controller;
- отсутствие side effects;
- передача конфигурации и внешних зависимостей.
Итоговый целевой набор:
```text
230 passed
```
Полная регрессия:
```text
1620 passed
```
End-to-end WebSocket и stress tests не подменяются unit-тестами
и остаются обязательной частью Build 060.25060.26.
---
## 20. Граница Build 060.25
Build 060.25 должен добавить production-владельца lifecycle:
```text
bootstrap
build composition
connect and subscribe
start supervisor and scheduler task
receive messages and notify activity
disconnect detected
reconnect and restore subscriptions
choose recovery_end_time
RuntimeRecoveryCoordinator.recover()
resume steady live lifecycle
```
До появления этой интеграции нельзя утверждать, что production
Trade Stream автоматически переживает WebSocket disconnect.
---
## 21. Что не входит в архитектуру Build 060.24
- production bootstrap;
- конкретный WebSocket lifecycle owner;
- автоматический вызов Runtime Recovery после reconnect;
- policy выбора `recovery_end_time`;
- retry/backoff;
- parallel recovery нескольких symbols;
- persistent checkpoint;
- долговременное Market Data Storage;
- historical query и replay services;
- integration, reconnect, recovery и stress tests полного процесса.
---
## 22. Итог
Build 060.24 формирует завершённый внутренний набор компонентов Runtime
Recovery и фиксирует их границы.
Архитектура:
- сохраняет checkpoint в единственном владельце;
- отделяет вычисление окон от выполнения Recovery;
- разделяет heartbeat, scheduling, supervision и reconnect;
- не смешивает Recovery с Transport;
- собирает live и recovery поверх общего Consistency Layer;
- не скрывает production lifecycle внутри factory-функции.
Следующий Build должен интегрировать эту архитектуру, а не менять её
ответственности.