Files
dzentra_bot/docs/migrations/build_060_24_architecture.md

27 KiB
Raw Blame History

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

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.25060.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 должен интегрировать эту архитектуру, а не менять её ответственности.