23 KiB
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 введено разделение между:
- каноническим хранилищем инструментов;
- 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 действуют следующие обязательные инварианты.
-
ExchangeService._exchange_symbols_cacheотсутствует в production-коде. -
Канонические справочные данные хранятся как:
tuple[Instrument, ...]
- Владельцем канонического состояния является:
InstrumentStore
- Текущей реализацией store является:
InMemoryInstrumentStore
ExchangeServiceзависит от абстракции:
InstrumentStoreProtocol
- Старый loader:
_load_exchange_symbols_via_acquisition()
отсутствует.
- Новый loader:
_load_instruments_via_acquisition()
возвращает только:
tuple[Instrument, ...]
- Новый loader не вызывает:
map_instruments_to_exchange_symbols()
-
Compatibility mapping выполняется после получения канонических моделей из store или acquisition pipeline.
-
_exchange_symbols_projection_cacheне является каноническим источником данных. -
Публичный контракт:
get_exchange_symbols() -> list[ExchangeSymbol]
сохранён для обратной совместимости.
-
validate_symbol()продолжает работать черезget_exchange_symbols(). -
При отключённой бирже store, projection cache и acquisition pipeline не используются.
-
Ошибка acquisition не заполняет канонический store.
-
Ошибка acquisition не заполняет projection cache.
-
Пустой
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 без нарушения работоспособности существующего бота.