# Build 060.24 — Runtime Recovery Architecture Specification **Статус:** Accepted **Build:** 060.24 **Подсистема:** Market Data Acquisition / Trade Stream Runtime --- ## 1. Назначение Документ фиксирует фактическую архитектуру Build 060.24 после завершения подэтапов 060.24.1–060.24.9.2. Build создаёт компоненты, необходимые для восстановления Trade Stream после потери активности, и связывает их в единый граф зависимостей. Документ отделяет: - завершённую внутреннюю архитектуру Build 060.24; - production lifecycle и end-to-end интеграцию Build 060.25; - интеграционные и стресс-сценарии Build 060.26; - постоянное хранение Build 060.27–060.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.25–060.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 должен интегрировать эту архитектуру, а не менять её ответственности.