Files
dzentra_bot/docs/migrations/build_020.md

863 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 канонические данные инструментов хранятся в специализированном:
```text
InMemoryInstrumentStore
```
в виде:
```text
tuple[Instrument, ...]
```
Legacy-модели `ExchangeSymbol` больше не являются каноническим представлением справочных данных.
---
## 2. Архитектурное состояние до Build 020
До миграции `ExchangeService` содержал:
```python
_exchange_symbols_cache: list[ExchangeSymbol] | None = None
```
Метод:
```python
get_exchange_symbols()
```
работал по следующей схеме:
```text
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-модель использовалась как каноническое хранилище
Каноническая модель:
```python
Instrument
```
содержит полные справочные данные инструмента.
Legacy-модель:
```python
ExchangeSymbol
```
является сокращённой compatibility-проекцией и содержит только часть этих данных.
Хранение только `ExchangeSymbol` означало потерю полноценного канонического представления после завершения acquisition pipeline.
### 3.2. Exchange Layer владел состоянием справочных данных
Поле:
```python
ExchangeService._exchange_symbols_cache
```
делало `ExchangeService` владельцем справочных данных инструментов.
Это противоречило целевой архитектуре:
```text
Acquisition Layer
Canonical Instrument Models
Storage Layer
Consumers / Compatibility Projections
```
### 3.3. Канонические данные и legacy-проекция были объединены
Результат acquisition pipeline немедленно преобразовывался:
```text
Instrument
ExchangeSymbol
```
и сохранялся только после преобразования.
Это не позволяло независимо использовать полный `Instrument` в будущих подсистемах Dzentra.
---
## 4. Реализованное архитектурное решение
В Build 020 введено разделение между:
1. каноническим хранилищем инструментов;
2. legacy compatibility projection cache.
Теперь `ExchangeService` использует:
```python
_instrument_store: InstrumentStoreProtocol = InMemoryInstrumentStore()
```
для хранения канонических моделей:
```python
tuple[Instrument, ...]
```
и отдельный:
```python
_exchange_symbols_projection_cache: list[ExchangeSymbol] | None = None
```
для временного кэширования legacy-проекции.
Итоговая схема:
```text
DzengiInstrumentDocumentSource
InstrumentFeed
DzengiInstrumentDocumentHandler
InstrumentAcquisitionService
tuple[Instrument, ...]
InMemoryInstrumentStore
Canonical Instrument Reference Data
map_instruments_to_exchange_symbols()
list[ExchangeSymbol]
Legacy Compatibility Projection Cache
```
---
## 5. Каноническое хранилище
Канонические справочные данные теперь находятся в:
```python
ExchangeService._instrument_store
```
Тип зависимости:
```python
InstrumentStoreProtocol
```
Текущая реализация:
```python
InMemoryInstrumentStore
```
Store хранит:
```python
tuple[Instrument, ...]
```
Ключ источника:
```text
dzengi
```
Таким образом, канонические справочные данные больше не зависят от legacy-модели `ExchangeSymbol`.
---
## 6. Новый production flow
Метод:
```python
get_exchange_symbols()
```
сохраняет прежний внешний контракт:
```python
list[ExchangeSymbol]
```
Это необходимо для сохранения работоспособности существующего бота во время поэтапной миграции.
Внутренний flow теперь выглядит следующим образом:
```text
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. Каноническое состояние
```python
_instrument_store
```
Хранит:
```python
tuple[Instrument, ...]
```
Назначение:
- хранение полной справочной модели инструмента;
- повторное использование канонических данных;
- основа для будущих market intelligence consumers;
- независимость от legacy exchange models.
### 7.2. Compatibility projection cache
```python
_exchange_symbols_projection_cache
```
Хранит:
```python
list[ExchangeSymbol] | None
```
Назначение:
- сохранить старый контракт `get_exchange_symbols()`;
- не выполнять повторный compatibility mapping при каждом вызове;
- обеспечить безопасную постепенную миграцию legacy-кода.
`_exchange_symbols_projection_cache` не является источником истины.
Источником истины является:
```text
InstrumentStore
```
---
## 8. Изменение acquisition loader
Старый метод:
```python
_load_exchange_symbols_via_acquisition()
```
удалён.
Он одновременно:
- запускал acquisition pipeline;
- получал `Instrument`;
- выполнял compatibility mapping;
- возвращал `ExchangeSymbol`.
Вместо него используется:
```python
_load_instruments_via_acquisition()
```
Новый метод возвращает:
```python
tuple[Instrument, ...]
```
и не выполняет:
```python
map_instruments_to_exchange_symbols()
```
Это обеспечивает чистую архитектурную границу:
```text
Acquisition Pipeline
Canonical Instrument Models
```
Compatibility mapping выполняется отдельно только там, где действительно требуется legacy-контракт.
---
## 9. Сохранение обратной совместимости
Build 020 не меняет публичный контракт:
```python
ExchangeService.get_exchange_symbols()
```
Он по-прежнему возвращает:
```python
list[ExchangeSymbol]
```
Благодаря этому продолжают работать существующие consumers, включая:
```text
validate_symbol()
currency_ui.py
legacy exchange UI
существующие unit tests
```
Миграция выполнена без обязательного одновременного переписывания всех legacy consumers.
---
## 10. Поведение при отключённой бирже
При:
```python
exchange_enabled = False
```
метод:
```python
get_exchange_symbols()
```
возвращает:
```python
[]
```
При этом он не должен:
- читать `InstrumentStore`;
- использовать существующий projection cache;
- запускать acquisition pipeline;
- обращаться к реальной бирже.
Это сохраняет прежнее поведение mock/disabled режима.
---
## 11. Поведение при cache hit
Если существует:
```python
_exchange_symbols_projection_cache
```
метод возвращает существующий объект legacy-проекции без:
- чтения acquisition source;
- повторной обработки документа;
- повторного compatibility mapping.
Если projection cache отсутствует, но канонические данные уже существуют в:
```python
InstrumentStore
```
то acquisition pipeline не запускается повторно.
Вместо этого выполняется:
```text
InstrumentStore
tuple[Instrument, ...]
Compatibility Mapper
list[ExchangeSymbol]
```
Пустой канонический набор:
```python
()
```
также считается валидным cache hit и не должен ошибочно интерпретироваться как отсутствие данных.
---
## 12. Поведение при cache miss
Если одновременно отсутствуют:
```text
_exchange_symbols_projection_cache
InstrumentStore entry for "dzengi"
```
выполняется полный production flow:
```text
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 сохраняется как:
```text
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. Канонические справочные данные хранятся как:
```python
tuple[Instrument, ...]
```
3. Владельцем канонического состояния является:
```text
InstrumentStore
```
4. Текущей реализацией store является:
```python
InMemoryInstrumentStore
```
5. `ExchangeService` зависит от абстракции:
```python
InstrumentStoreProtocol
```
6. Старый loader:
```python
_load_exchange_symbols_via_acquisition()
```
отсутствует.
7. Новый loader:
```python
_load_instruments_via_acquisition()
```
возвращает только:
```python
tuple[Instrument, ...]
```
8. Новый loader не вызывает:
```python
map_instruments_to_exchange_symbols()
```
9. Compatibility mapping выполняется после получения канонических моделей из store или acquisition pipeline.
10. `_exchange_symbols_projection_cache` не является каноническим источником данных.
11. Публичный контракт:
```python
get_exchange_symbols() -> list[ExchangeSymbol]
```
сохранён для обратной совместимости.
12. `validate_symbol()` продолжает работать через `get_exchange_symbols()`.
13. При отключённой бирже store, projection cache и acquisition pipeline не используются.
14. Ошибка acquisition не заполняет канонический store.
15. Ошибка acquisition не заполняет projection cache.
16. Пустой `tuple[Instrument, ...]` является валидным сохранённым значением и должен отличаться от отсутствия записи в store.
---
## 15. Изменённые production-файлы
Основные изменения Build 020 выполнены в:
```text
app/src/integrations/exchange/service.py
```
Используются ранее подготовленные компоненты Storage Layer:
```text
app/src/storage/instrument_store.py
app/src/storage/exceptions.py
```
Используется compatibility mapper:
```text
app/src/market_data/acquisition/compatibility.py
```
---
## 16. Тестовое покрытие
Основные проверки миграции находятся в:
```text
app/tests/unit/integrations/exchange/test_service_exchange_symbols.py
```
Дополнительно проверена совместимость:
```text
app/tests/unit/integrations/exchange/test_service_validate_symbol.py
```
И отдельно сохраняется тестовое покрытие самого store:
```text
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()`:
```text
23 passed in 0.12s
```
Целевая проверка `validate_symbol()`:
```text
16 passed in 0.07s
```
Проверка синтаксической компиляции:
```text
py_compile — успешно
```
Полный regression suite:
```text
426 passed in 0.23s
```
Финальная архитектурная проверка подтвердила:
```text
старый _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:
```text
Exchange API
Acquisition Pipeline
Instrument
Compatibility Mapper
ExchangeSymbol
ExchangeService class-level cache
```
После Build 020:
```text
Exchange API
Acquisition Pipeline
Instrument
InstrumentStore
Canonical Instrument Reference Data
Compatibility Mapper
ExchangeSymbol
Temporary Legacy Projection Cache
```
Ключевое изменение:
```text
ExchangeSymbol больше не является канонически сохраняемой моделью Instrument Reference Data.
```
Каноническим представлением теперь является:
```python
Instrument
```
а владельцем его состояния является:
```text
Storage Layer
```
---
## 19. Ограничения текущего этапа
Build 020 намеренно не выполняет полную миграцию всех consumers на каноническую модель `Instrument`.
На текущем этапе сохраняются:
```python
get_exchange_symbols() -> list[ExchangeSymbol]
```
и:
```python
_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 считается завершённым, если одновременно выполняются следующие условия:
- [x] созданный ранее `InstrumentStore` используется production-кодом;
- [x] канонические данные хранятся как `tuple[Instrument, ...]`;
- [x] старый `_exchange_symbols_cache` удалён;
- [x] старый `_load_exchange_symbols_via_acquisition()` удалён;
- [x] новый `_load_instruments_via_acquisition()` возвращает канонические модели;
- [x] acquisition loader не выполняет compatibility mapping;
- [x] legacy API `get_exchange_symbols()` сохранён;
- [x] `validate_symbol()` сохраняет прежнее поведение;
- [x] ошибки acquisition не создают ложное состояние кэша;
- [x] целевые тесты проходят;
- [x] `py_compile` проходит;
- [x] полный regression suite проходит;
- [x] финальная архитектурная grep-проверка выполнена.
---
## 21. Итоговый статус
```text
BUILD 020 — COMPLETE
```
Build 020 завершает фактический перенос канонического кэша Instrument Reference Data из legacy `ExchangeService._exchange_symbols_cache` в специализированный Storage Layer.
Существующий бот продолжает работать через сохранённый compatibility-контракт:
```python
get_exchange_symbols() -> list[ExchangeSymbol]
```
При этом новая архитектура уже располагает полноценным каноническим хранилищем:
```text
Instrument Acquisition Pipeline
tuple[Instrument, ...]
InstrumentStore
```
Это создаёт основу для дальнейшего поэтапного перевода consumers с legacy `ExchangeSymbol` на каноническую модель `Instrument` без нарушения работоспособности существующего бота.