27 KiB
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
WebSocket Transport
│
▼
AcquisitionRuntimeService
│
▼
TradeStreamMessageAdapterProtocol
│
▼
TradeStreamAcquisitionService
│
▼
TradeStreamConsistencyController
│
▼
Canonical Trade | None
2.2. Recovery pipeline
TradeRecoveryRequest
│
▼
TradeRecoveryController
│
▼
DzengiTradesDocumentSource
│
▼
TradeRecoveryNormalizer
│
▼
TradeStreamConsistencyController
│
▼
Canonical Trade[]
2.3. Проблема до Build
До Build компоненты Live и Recovery существовали отдельно. Отсутствовали:
- надёжная временная точка начала восстановления;
- разбиение диапазона на допустимые REST-окна;
- контроль потери runtime-активности;
- одна формализованная reconnect-попытка;
- состояние runtime-сессии;
- периодический вызов heartbeat;
- координатор checkpoint → recovery;
- composition root с общими stateful-зависимостями.
3. Итоговая модель
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. Слои и направления зависимостей
Допустимое направление:
Composition
│
├── Runtime
├── Recovery
├── Consistency
└── Acquisition
Runtime Recovery Coordinator
│
├── State Store Protocol
├── Window Planner
└── Recovery Protocol
Recovery Controller
│
└── Consistency Protocol
Acquisition Service
│
└── Consistency Protocol
Запрещённые обратные зависимости:
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
Владелец:
TradeStreamStateStore
│
▼
TradeStreamState(symbol)
Содержит:
- последнюю принятую сделку;
- идентификатор последней принятой сделки;
- ограниченное окно deduplication.
5.2. Runtime session state
Отдельный RuntimeSessionState не создаётся.
Общее состояние lifecycle принадлежит Supervisor:
RuntimeSupervisorState
├── STOPPED
├── RUNNING
├── RECONNECTING
└── FAILED
Локальное состояние операции reconnect принадлежит
ReconnectCoordinator:
ReconnectState
├── DISCONNECTED
├── CONNECTING
├── RESTORING_SUBSCRIPTIONS
├── CONNECTED
└── FAILED
Состояние контроля активности принадлежит Heartbeat:
HeartbeatState
├── IDLE
├── MONITORING
└── TIMED_OUT
Это разделение предотвращает появление общего mutable-объекта, дублирующего состояние специализированных компонентов.
5.3. Scheduler state
Scheduler хранит только:
running: bool
Флаг описывает состояние его цикла и не является состоянием WebSocket-сессии.
6. RuntimeCheckpoint
6.1. Владелец
Checkpoint принадлежит только:
TradeStreamState.last_trade
Runtime, Recovery Coordinator и Supervisor могут читать checkpoint, но не владеют им и не изменяют его.
6.2. Причина хранения полного Trade
Полный канонический Trade уже содержит согласованные:
symbol
trade_id
executed_at
price
quantity
side
source
Отдельные поля checkpoint создавали бы риск рассинхронизации.
6.3. Инварианты
last_trade is None
⇔
last_trade_id is None
last_trade.trade_id == last_trade_id
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:
TradeRecoveryWindow(
symbol: str,
start_time: int,
end_time: int,
)
Свойства:
- immutable;
- slots;
- валиден сразу после создания;
- независим от REST, Runtime и конкретной биржи.
7.2. TradeRecoveryWindowPlanner
Planner объединяет роли RecoveryPlanner и RecoveryWindowCalculator:
build_windows(
symbol,
start_time,
end_time,
) -> tuple[TradeRecoveryWindow, ...]
Отдельный Calculator не вводится, потому что расчёт окон:
- не имеет собственного состояния;
- не имеет политики за пределами Planner;
- не используется независимо от построения recovery plan.
7.3. Временные границы
start_time и end_time представлены Unix milliseconds.
Допустимый диапазон:
0 <= start_time <= end_time
Равные границы означают отсутствие необходимого восстановления:
start_time == end_time
│
▼
()
7.4. Размер окна
Ограничение Recovery Request:
end_time - start_time < 3_600_000
Максимальное значение Planner:
3_599_999 ms
7.5. Непрерывность
Planner строит последовательность:
[start, boundary_1]
[boundary_1, boundary_2]
[boundary_2, end]
Соседние окна разделяют одну границу. Это исключает временной разрыв. Повтор сделки на общей границе безопасен, потому что Recovery использует тот же Consistency Layer.
8. Reconnect
8.1. Ответственность
ReconnectCoordinator.reconnect() выполняет ровно одну попытку:
attempt += 1
│
▼
publish ReconnectStartedEvent
│
▼
dispatch ConnectCommand
│
▼
restore_subscriptions()
│
▼
publish ReconnectCompletedEvent
8.2. Ошибка
connect or restore error
│
▼
state = FAILED
│
▼
publish ReconnectFailedEvent
│
▼
raise original exception
8.3. Граница
Reconnect не решает:
- когда запускаться;
- сколько раз повторяться;
- какой backoff использовать;
- требуется ли Trade Recovery;
- когда возобновлять live processing.
9. Heartbeat
Heartbeat — пассивный детектор отсутствия активности.
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. Запуск
STOPPED
│ start()
▼
heartbeat.start()
│
▼
RUNNING
10.2. Активность
notify_activity() передаётся Heartbeat только в состоянии RUNNING.
Активность после остановки или во время reconnect не запускает monitor
неявно.
10.3. Timeout
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 владеет только временем вызовов:
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. Контракт
recover(
symbol: str,
recovery_end_time: int,
) -> TradeRecoveryResult
RuntimeRecoveryProtocol является structural protocol. Реализация
не наследует Protocol напрямую, что сохраняет рабочий __slots__
и позволяет проверять совместимость через runtime_checkable.
12.2. Последовательность
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.
Конечная граница передаётся извне:
recovery_end_time
Coordinator не читает часы самостоятельно. Это оставляет production policy владельцу lifecycle и обеспечивает детерминированные тесты.
executed_at обязан быть timezone-aware. Преобразование в Unix
milliseconds выполняется через UTC epoch и целочисленную арифметику.
12.4. Empty recovery
Если state или checkpoint отсутствуют:
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. Создаваемые объекты
TradeStreamStateStore
TradeStreamConsistencyController
TradeRecoveryController
TradeRecoveryWindowPlanner
RuntimeRecoveryCoordinator
AcquisitionRuntimeService
TradeStreamAcquisitionService
ReconnectCoordinator
HeartbeatMonitor
RuntimeSupervisor
RuntimeScheduler
13.3. Identity graph
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-контракты:
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 не создают неявных фоновых задач.
Единственный длительный цикл находится в:
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;
- передача конфигурации и внешних зависимостей.
Итоговый целевой набор:
230 passed
Полная регрессия:
1620 passed
End-to-end WebSocket и stress tests не подменяются unit-тестами и остаются обязательной частью Build 060.25–060.26.
20. Граница Build 060.25
Build 060.25 должен добавить production-владельца lifecycle:
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 должен интегрировать эту архитектуру, а не менять её ответственности.