Files
dzentra_bot/docs/migrations/build_020.md

23 KiB
Raw Blame History

Build 020 — Перенос кэша инструментов в Storage Layer

Engineering Migration Record


Контроль документа

Свойство Значение
Документ Build 020 — Перенос кэша инструментов в Storage Layer
Тип документа Engineering Migration Record
Статус Complete
Проект Dzentra
Подсистема Instrument Reference Data
Build 020
Язык Русский
Целевой файл docs/migrations/instrument_reference_data/build_020.md

1. Цель Build 020

Цель Build 020 — удалить каноническое хранение справочных данных инструментов из legacy-кэша ExchangeService._exchange_symbols_cache и перенести его в специализированный Storage Layer.

До выполнения Build 020 ExchangeService одновременно отвечал за:

  • получение справочных данных инструментов;
  • запуск acquisition pipeline;
  • преобразование канонических моделей Instrument в legacy-модели ExchangeSymbol;
  • хранение результата в собственном class-level cache.

Это создавало архитектурную зависимость канонических справочных данных от legacy exchange layer.

После Build 020 канонические данные инструментов хранятся в специализированном:

InMemoryInstrumentStore

в виде:

tuple[Instrument, ...]

Legacy-модели ExchangeSymbol больше не являются каноническим представлением справочных данных.


2. Архитектурное состояние до Build 020

До миграции ExchangeService содержал:

_exchange_symbols_cache: list[ExchangeSymbol] | None = None

Метод:

get_exchange_symbols()

работал по следующей схеме:

get_exchange_symbols()
    ↓
_exchange_symbols_cache
    ↓ cache miss
_load_exchange_symbols_via_acquisition()
    ↓
Instrument Acquisition Pipeline
    ↓
tuple[Instrument, ...]
    ↓
map_instruments_to_exchange_symbols()
    ↓
list[ExchangeSymbol]
    ↓
_exchange_symbols_cache

Таким образом, результат новой Instrument Reference Data pipeline сразу преобразовывался в legacy-модель, и именно legacy-представление сохранялось как основной кэш.


3. Архитектурная проблема

Старый подход имел несколько принципиальных недостатков.

3.1. Legacy-модель использовалась как каноническое хранилище

Каноническая модель:

Instrument

содержит полные справочные данные инструмента.

Legacy-модель:

ExchangeSymbol

является сокращённой compatibility-проекцией и содержит только часть этих данных.

Хранение только ExchangeSymbol означало потерю полноценного канонического представления после завершения acquisition pipeline.

3.2. Exchange Layer владел состоянием справочных данных

Поле:

ExchangeService._exchange_symbols_cache

делало ExchangeService владельцем справочных данных инструментов.

Это противоречило целевой архитектуре:

Acquisition Layer
    ↓
Canonical Instrument Models
    ↓
Storage Layer
    ↓
Consumers / Compatibility Projections

3.3. Канонические данные и legacy-проекция были объединены

Результат acquisition pipeline немедленно преобразовывался:

Instrument
    ↓
ExchangeSymbol

и сохранялся только после преобразования.

Это не позволяло независимо использовать полный Instrument в будущих подсистемах Dzentra.


4. Реализованное архитектурное решение

В Build 020 введено разделение между:

  1. каноническим хранилищем инструментов;
  2. legacy compatibility projection cache.

Теперь ExchangeService использует:

_instrument_store: InstrumentStoreProtocol = InMemoryInstrumentStore()

для хранения канонических моделей:

tuple[Instrument, ...]

и отдельный:

_exchange_symbols_projection_cache: list[ExchangeSymbol] | None = None

для временного кэширования legacy-проекции.

Итоговая схема:

DzengiInstrumentDocumentSource
    ↓
InstrumentFeed
    ↓
DzengiInstrumentDocumentHandler
    ↓
InstrumentAcquisitionService
    ↓
tuple[Instrument, ...]
    ↓
InMemoryInstrumentStore
    ↓
Canonical Instrument Reference Data
    ↓
map_instruments_to_exchange_symbols()
    ↓
list[ExchangeSymbol]
    ↓
Legacy Compatibility Projection Cache

5. Каноническое хранилище

Канонические справочные данные теперь находятся в:

ExchangeService._instrument_store

Тип зависимости:

InstrumentStoreProtocol

Текущая реализация:

InMemoryInstrumentStore

Store хранит:

tuple[Instrument, ...]

Ключ источника:

dzengi

Таким образом, канонические справочные данные больше не зависят от legacy-модели ExchangeSymbol.


6. Новый production flow

Метод:

get_exchange_symbols()

сохраняет прежний внешний контракт:

list[ExchangeSymbol]

Это необходимо для сохранения работоспособности существующего бота во время поэтапной миграции.

Внутренний flow теперь выглядит следующим образом:

get_exchange_symbols()
    ↓
Проверка exchange_enabled
    ↓
Проверка _exchange_symbols_projection_cache
    ↓ cache miss
Чтение InstrumentStore
    ↓ store miss
_load_instruments_via_acquisition()
    ↓
tuple[Instrument, ...]
    ↓
Сохранение в InstrumentStore
    ↓
map_instruments_to_exchange_symbols()
    ↓
list[ExchangeSymbol]
    ↓
Сохранение в _exchange_symbols_projection_cache
    ↓
Возврат legacy-результата

7. Разделение канонического кэша и compatibility projection cache

После Build 020 существуют два разных уровня состояния.

7.1. Каноническое состояние

_instrument_store

Хранит:

tuple[Instrument, ...]

Назначение:

  • хранение полной справочной модели инструмента;
  • повторное использование канонических данных;
  • основа для будущих market intelligence consumers;
  • независимость от legacy exchange models.

7.2. Compatibility projection cache

_exchange_symbols_projection_cache

Хранит:

list[ExchangeSymbol] | None

Назначение:

  • сохранить старый контракт get_exchange_symbols();
  • не выполнять повторный compatibility mapping при каждом вызове;
  • обеспечить безопасную постепенную миграцию legacy-кода.

_exchange_symbols_projection_cache не является источником истины.

Источником истины является:

InstrumentStore

8. Изменение acquisition loader

Старый метод:

_load_exchange_symbols_via_acquisition()

удалён.

Он одновременно:

  • запускал acquisition pipeline;
  • получал Instrument;
  • выполнял compatibility mapping;
  • возвращал ExchangeSymbol.

Вместо него используется:

_load_instruments_via_acquisition()

Новый метод возвращает:

tuple[Instrument, ...]

и не выполняет:

map_instruments_to_exchange_symbols()

Это обеспечивает чистую архитектурную границу:

Acquisition Pipeline
    ↓
Canonical Instrument Models

Compatibility mapping выполняется отдельно только там, где действительно требуется legacy-контракт.


9. Сохранение обратной совместимости

Build 020 не меняет публичный контракт:

ExchangeService.get_exchange_symbols()

Он по-прежнему возвращает:

list[ExchangeSymbol]

Благодаря этому продолжают работать существующие consumers, включая:

validate_symbol()
currency_ui.py
legacy exchange UI
существующие unit tests

Миграция выполнена без обязательного одновременного переписывания всех legacy consumers.


10. Поведение при отключённой бирже

При:

exchange_enabled = False

метод:

get_exchange_symbols()

возвращает:

[]

При этом он не должен:

  • читать InstrumentStore;
  • использовать существующий projection cache;
  • запускать acquisition pipeline;
  • обращаться к реальной бирже.

Это сохраняет прежнее поведение mock/disabled режима.


11. Поведение при cache hit

Если существует:

_exchange_symbols_projection_cache

метод возвращает существующий объект legacy-проекции без:

  • чтения acquisition source;
  • повторной обработки документа;
  • повторного compatibility mapping.

Если projection cache отсутствует, но канонические данные уже существуют в:

InstrumentStore

то acquisition pipeline не запускается повторно.

Вместо этого выполняется:

InstrumentStore
    ↓
tuple[Instrument, ...]
    ↓
Compatibility Mapper
    ↓
list[ExchangeSymbol]

Пустой канонический набор:

()

также считается валидным cache hit и не должен ошибочно интерпретироваться как отсутствие данных.


12. Поведение при cache miss

Если одновременно отсутствуют:

_exchange_symbols_projection_cache
InstrumentStore entry for "dzengi"

выполняется полный production flow:

Instrument Source
    ↓
Instrument Handler
    ↓
Instrument Acquisition Service
    ↓
tuple[Instrument, ...]
    ↓
InstrumentStore.set(...)
    ↓
Compatibility Mapper
    ↓
list[ExchangeSymbol]

После успешной загрузки:

  • канонический tuple[Instrument, ...] сохраняется в InstrumentStore;
  • legacy-проекция создаётся отдельно;
  • legacy-проекция сохраняется в _exchange_symbols_projection_cache.

13. Поведение при ошибках

Если acquisition pipeline завершается ошибкой:

  • ошибка логируется как exchange request error;
  • endpoint сохраняется как:
exchangeInfo
  • исходная ошибка становится причиной ExchangeError;
  • канонический store не должен получать частичные или некорректные данные;
  • projection cache не должен заполняться.

Таким образом, ошибка acquisition не создаёт ложное успешное состояние.

Если ошибка возникает на этапе compatibility mapping:

  • канонические данные уже могут находиться в InstrumentStore;
  • projection cache не должен заполняться некорректным результатом;
  • следующий вызов может повторно построить legacy-проекцию из сохранённых канонических данных без повторного запуска acquisition pipeline.

14. Инварианты Build 020

После завершения Build 020 действуют следующие обязательные инварианты.

  1. ExchangeService._exchange_symbols_cache отсутствует в production-коде.

  2. Канонические справочные данные хранятся как:

tuple[Instrument, ...]
  1. Владельцем канонического состояния является:
InstrumentStore
  1. Текущей реализацией store является:
InMemoryInstrumentStore
  1. ExchangeService зависит от абстракции:
InstrumentStoreProtocol
  1. Старый loader:
_load_exchange_symbols_via_acquisition()

отсутствует.

  1. Новый loader:
_load_instruments_via_acquisition()

возвращает только:

tuple[Instrument, ...]
  1. Новый loader не вызывает:
map_instruments_to_exchange_symbols()
  1. Compatibility mapping выполняется после получения канонических моделей из store или acquisition pipeline.

  2. _exchange_symbols_projection_cache не является каноническим источником данных.

  3. Публичный контракт:

get_exchange_symbols() -> list[ExchangeSymbol]

сохранён для обратной совместимости.

  1. validate_symbol() продолжает работать через get_exchange_symbols().

  2. При отключённой бирже store, projection cache и acquisition pipeline не используются.

  3. Ошибка acquisition не заполняет канонический store.

  4. Ошибка acquisition не заполняет projection cache.

  5. Пустой tuple[Instrument, ...] является валидным сохранённым значением и должен отличаться от отсутствия записи в store.


15. Изменённые production-файлы

Основные изменения Build 020 выполнены в:

app/src/integrations/exchange/service.py

Используются ранее подготовленные компоненты Storage Layer:

app/src/storage/instrument_store.py
app/src/storage/exceptions.py

Используется compatibility mapper:

app/src/market_data/acquisition/compatibility.py

16. Тестовое покрытие

Основные проверки миграции находятся в:

app/tests/unit/integrations/exchange/test_service_exchange_symbols.py

Дополнительно проверена совместимость:

app/tests/unit/integrations/exchange/test_service_validate_symbol.py

И отдельно сохраняется тестовое покрытие самого store:

app/tests/unit/storage/test_instrument_store.py

Проверены следующие сценарии:

  • отключённая биржа возвращает пустой список;
  • отключённая биржа не читает существующий InstrumentStore;
  • cache hit legacy-проекции не запускает acquisition pipeline;
  • store hit не запускает acquisition pipeline;
  • store miss запускает acquisition pipeline;
  • загруженные Instrument сохраняются в store;
  • пустой tuple корректно обрабатывается как cache hit;
  • compatibility mapping получает именно канонические Instrument;
  • legacy-проекция сохраняет прежний тип list[ExchangeSymbol];
  • сохраняется порядок инструментов;
  • acquisition error оборачивается в ExchangeError;
  • acquisition error логируется с endpoint exchangeInfo;
  • acquisition error не заполняет store;
  • acquisition error не заполняет projection cache;
  • acquisition loader использует реальную processing pipeline;
  • acquisition loader использует registry key dzengi;
  • acquisition loader возвращает tuple[Instrument, ...];
  • acquisition loader не вызывает compatibility mapper;
  • validate_symbol() сохраняет прежнее поведение.

17. Результаты проверок

Целевая проверка get_exchange_symbols():

23 passed in 0.12s

Целевая проверка validate_symbol():

16 passed in 0.07s

Проверка синтаксической компиляции:

py_compile — успешно

Полный regression suite:

426 passed in 0.23s

Финальная архитектурная проверка подтвердила:

старый _exchange_symbols_cache отсутствует в production-коде;
старый _load_exchange_symbols_via_acquisition отсутствует;
канонический store подключён через InstrumentStoreProtocol;
текущая реализация store — InMemoryInstrumentStore;
legacy projection cache отделён от канонического store;
compatibility mapper не вызывается внутри acquisition loader;
новый acquisition loader возвращает канонические Instrument.

18. Архитектурный результат

До Build 020:

Exchange API
    ↓
Acquisition Pipeline
    ↓
Instrument
    ↓
Compatibility Mapper
    ↓
ExchangeSymbol
    ↓
ExchangeService class-level cache

После Build 020:

Exchange API
    ↓
Acquisition Pipeline
    ↓
Instrument
    ↓
InstrumentStore
    ↓
Canonical Instrument Reference Data
    ↓
Compatibility Mapper
    ↓
ExchangeSymbol
    ↓
Temporary Legacy Projection Cache

Ключевое изменение:

ExchangeSymbol больше не является канонически сохраняемой моделью Instrument Reference Data.

Каноническим представлением теперь является:

Instrument

а владельцем его состояния является:

Storage Layer

19. Ограничения текущего этапа

Build 020 намеренно не выполняет полную миграцию всех consumers на каноническую модель Instrument.

На текущем этапе сохраняются:

get_exchange_symbols() -> list[ExchangeSymbol]

и:

_exchange_symbols_projection_cache

Они являются compatibility-механизмами переходного периода.

Также Build 020 не переносит в новый InstrumentStore:

  • market price cache;
  • execution price cache;
  • balance snapshots;
  • journal storage;
  • другие runtime caches.

Build 020 касается исключительно хранения канонических Instrument Reference Data.


20. Критерии завершения

Build 020 считается завершённым, если одновременно выполняются следующие условия:

  • созданный ранее InstrumentStore используется production-кодом;
  • канонические данные хранятся как tuple[Instrument, ...];
  • старый _exchange_symbols_cache удалён;
  • старый _load_exchange_symbols_via_acquisition() удалён;
  • новый _load_instruments_via_acquisition() возвращает канонические модели;
  • acquisition loader не выполняет compatibility mapping;
  • legacy API get_exchange_symbols() сохранён;
  • validate_symbol() сохраняет прежнее поведение;
  • ошибки acquisition не создают ложное состояние кэша;
  • целевые тесты проходят;
  • py_compile проходит;
  • полный regression suite проходит;
  • финальная архитектурная grep-проверка выполнена.

21. Итоговый статус

BUILD 020 — COMPLETE

Build 020 завершает фактический перенос канонического кэша Instrument Reference Data из legacy ExchangeService._exchange_symbols_cache в специализированный Storage Layer.

Существующий бот продолжает работать через сохранённый compatibility-контракт:

get_exchange_symbols() -> list[ExchangeSymbol]

При этом новая архитектура уже располагает полноценным каноническим хранилищем:

Instrument Acquisition Pipeline
    ↓
tuple[Instrument, ...]
    ↓
InstrumentStore

Это создаёт основу для дальнейшего поэтапного перевода consumers с legacy ExchangeSymbol на каноническую модель Instrument без нарушения работоспособности существующего бота.