Files
dzentra_bot/docs/migrations/build_019.md

618 lines
14 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 019 — Подготовка переноса кэша в Storage
## Статус
**COMPLETE**
---
## Цель
Подготовить независимый storage-контракт для хранения канонического справочника инструментов перед последующим переносом legacy-кэша:
```python
ExchangeService._exchange_symbols_cache
```
из слоя:
```text
src/integrations/exchange
```
в слой:
```text
src/storage
```
без изменения текущего production-пути и без нарушения работы существующего бота.
---
## Архитектурный принцип
До Build 019 кэш справочника торговых инструментов находился непосредственно внутри legacy-интеграционного сервиса:
```python
ExchangeService._exchange_symbols_cache
```
Это создаёт архитектурную связь между:
- получением Instrument Reference Data;
- legacy-моделью `ExchangeSymbol`;
- интеграционным слоем биржи;
- runtime-хранением загруженного справочника.
В новой архитектуре ответственность разделяется:
```text
Market Data Acquisition
tuple[Instrument, ...]
Storage
Compatibility Layer
Legacy ExchangeSymbol
```
Build 019 создаёт только новый storage-контракт и его in-memory реализацию.
Переключение production-кода на новый store в рамках Build 019 не выполняется.
---
## Созданные файлы
```text
app/src/storage/exceptions.py
app/src/storage/instrument_store.py
app/tests/unit/storage/test_instrument_store.py
```
---
## `src/storage/exceptions.py`
Создана базовая иерархия ошибок storage-слоя:
```text
Exception
StorageError
InstrumentStoreError
```
### `StorageError`
Базовая ошибка storage-слоя.
### `InstrumentStoreError`
Специализированная ошибка операций и нарушений контракта хранилища справочника инструментов.
---
## `src/storage/instrument_store.py`
Созданы:
```text
InstrumentStoreProtocol
InMemoryInstrumentStore
```
### `InstrumentStoreProtocol`
Определяет независимый контракт runtime-хранилища канонического справочника инструментов.
Поддерживаемые операции:
```python
get(
source_name: str,
) -> tuple[Instrument, ...] | None
```
```python
set(
source_name: str,
instruments: tuple[Instrument, ...],
) -> None
```
```python
clear(
source_name: str | None = None,
) -> None
```
Контракт не зависит от:
- `ExchangeService`;
- `ExchangeSymbol`;
- Dzengi REST API;
- PostgreSQL;
- Redis;
- Telegram UI;
- legacy compatibility mapper.
---
## Семантика `get()`
Метод:
```python
get(source_name)
```
возвращает:
```text
None
```
если данные для источника никогда не сохранялись.
Это означает:
```text
cache miss
```
Если в store был успешно сохранён пустой справочник:
```python
()
```
метод возвращает именно:
```python
()
```
Таким образом:
```text
None != ()
```
и состояния:
```text
данные отсутствуют
```
и:
```text
успешно загружен пустой справочник
```
не смешиваются.
---
## Семантика `set()`
Метод принимает исключительно:
```python
tuple[Instrument, ...]
```
Это сохраняет immutable-контракт новой модели Instrument Reference Data.
Store не выполняет:
- копирование tuple;
- сортировку;
- преобразование элементов;
- mapping в `ExchangeSymbol`;
- нормализацию `Instrument`;
- изменение порядка элементов.
Сохраняется исходный объект tuple.
Следовательно:
```python
store.set("dzengi", instruments)
assert store.get("dzengi") is instruments
```
---
## Семантика `clear()`
Поддерживаются два режима.
Очистка конкретного источника:
```python
store.clear("dzengi")
```
Полная очистка store:
```python
store.clear()
```
Очистка неизвестного источника является идемпотентной и не вызывает ошибку.
---
## Изоляция источников
Store поддерживает независимое хранение нескольких источников:
```text
dzengi
secondary
other-source
```
Например:
```text
InMemoryInstrumentStore
├── dzengi
│ └── tuple[Instrument, ...]
└── secondary
└── tuple[Instrument, ...]
```
Изменение или очистка одного источника не влияет на остальные.
---
## Нормализация имени источника
Внешние пробелы удаляются:
```text
" dzengi "
```
нормализуется в:
```text
"dzengi"
```
Регистр сохраняется.
Следовательно:
```text
dzengi
```
и:
```text
DZENGI
```
являются разными ключами.
Пустые имена источников запрещены:
```text
""
" "
" "
"\t"
"\n"
```
и приводят к:
```python
InstrumentStoreError
```
---
## Runtime-валидация
`InMemoryInstrumentStore.set()` проверяет:
1. что набор передан как `tuple`;
2. что каждый элемент является экземпляром `Instrument`.
Нарушение контракта приводит к:
```python
InstrumentStoreError
```
---
## Что намеренно не изменялось
Build 019 не изменяет:
```text
app/src/integrations/exchange/service.py
app/src/market_data/acquisition/service.py
app/src/market_data/acquisition/compatibility.py
app/src/storage/session.py
app/src/storage/schema.py
app/src/storage/models.py
app/src/storage/repositories/*
app/src/storage/__init__.py
```
Не добавлялись:
- PostgreSQL-таблицы;
- Redis;
- новый database repository;
- dependency injection в `ExchangeService`;
- production singleton store;
- глобальный storage registry.
---
## Legacy-кэш
После Build 019 legacy-кэш остаётся на прежнем месте:
```python
class ExchangeService:
_exchange_symbols_cache: list[ExchangeSymbol] | None = None
```
Текущий production-путь остаётся неизменным:
```text
ExchangeService.get_exchange_symbols()
├── cache hit
│ │
│ ▼
│ _exchange_symbols_cache
└── cache miss
_load_exchange_symbols_via_acquisition()
InstrumentAcquisitionService
tuple[Instrument, ...]
map_instruments_to_exchange_symbols()
list[ExchangeSymbol]
_exchange_symbols_cache
```
Новый `InMemoryInstrumentStore` в этот production-путь пока не подключён.
---
## Тестовое покрытие
Создан файл:
```text
app/tests/unit/storage/test_instrument_store.py
```
Проверены:
- соответствие `InstrumentStoreProtocol`;
- cache miss;
- сохранение и получение данных;
- сохранение identity исходного tuple;
- различие между `None` и пустым tuple;
- изоляция разных источников;
- очистка одного источника;
- полная очистка store;
- замена ранее сохранённого значения;
- нормализация внешних пробелов имени источника;
- сохранение регистра имени источника;
- отклонение пустых имён источников;
- отклонение списка вместо tuple;
- отклонение объектов, не являющихся `Instrument`;
- сохранение порядка инструментов;
- отсутствие изменения входного tuple;
- изоляция разных экземпляров store;
- идемпотентная очистка неизвестного источника;
- наследование `InstrumentStoreError` от `StorageError`.
---
## Результаты проверок
### Unit-тесты нового store
Команда:
```bash
python -m pytest \
tests/unit/storage/test_instrument_store.py \
-q
```
Результат:
```text
32 passed in 0.02s
```
---
### Проверка компиляции
Команда:
```bash
python -m py_compile \
src/storage/exceptions.py \
src/storage/instrument_store.py \
tests/unit/storage/test_instrument_store.py
```
Результат:
```text
успешно
```
---
### Полный regression suite
Команда:
```bash
python -m pytest -q
```
Результат:
```text
419 passed in 0.21s
```
---
## Архитектурная проверка
Выполнена команда:
```bash
grep -RIn \
--exclude-dir="__pycache__" \
--exclude="*.pyc" \
-E "InstrumentStoreProtocol|InMemoryInstrumentStore|InstrumentStoreError|StorageError|_exchange_symbols_cache|instrument_store" \
src tests
```
Проверка подтвердила:
- `InstrumentStoreProtocol` определён в `src/storage/instrument_store.py`;
- `InMemoryInstrumentStore` определён в `src/storage/instrument_store.py`;
- `StorageError` и `InstrumentStoreError` находятся в `src/storage/exceptions.py`;
- новый store используется только собственными unit-тестами;
- production-код на новый store не переключён;
- `_exchange_symbols_cache` остаётся в `ExchangeService`;
- существующие legacy-тесты кэша продолжают работать.
---
## Итоговая архитектура после Build 019
```text
External Exchange API
Market Data Acquisition
tuple[Instrument, ...]
├──────────────────────────────┐
│ │
▼ ▼
InMemoryInstrumentStore Compatibility Layer
│ │
│ ▼
│ list[ExchangeSymbol]
│ │
│ ▼
│ ExchangeService._exchange_symbols_cache
готов к будущему подключению
в production-путь
```
На текущем этапе новый store существует независимо от legacy-кэша.
---
## Граница Build 019
Build 019 считается завершённым, потому что:
1. создан независимый storage-контракт для `Instrument`;
2. создана in-memory реализация store;
3. сохранена семантика immutable `tuple[Instrument, ...]`;
4. определено различие между cache miss и пустым справочником;
5. обеспечена изоляция источников;
6. добавлена специализированная иерархия storage-ошибок;
7. production-код не изменён;
8. legacy-кэш не удалён;
9. полный regression suite проходит успешно.
---
## Следующий шаг
Следующий логический этап:
```text
Build 020 — Подключение Instrument Store к production-пути
```
Цель следующего этапа:
```text
переключить хранение канонического tuple[Instrument, ...]
с legacy-кэша ExchangeService
на InMemoryInstrumentStore
```
при сохранении внешнего legacy-контракта:
```python
ExchangeService.get_exchange_symbols() -> list[ExchangeSymbol]
```
и без нарушения работы существующего бота.
На Build 020 необходимо отдельно определить:
- где создаётся production-экземпляр `InMemoryInstrumentStore`;
- как `ExchangeService` получает доступ к нему;
- сохраняется ли временно `_exchange_symbols_cache` как compatibility-кэш;
- в какой точке выполняется mapping `Instrument -> ExchangeSymbol`;
- как сохранить существующую identity-семантику `get_exchange_symbols()`;
- как обеспечить безопасный rollback без изменения внешнего API.
---
## Итог
**Build 019 завершён успешно.**
Новый storage-контракт создан и полностью покрыт unit-тестами.
Существующий бот продолжает использовать прежний production-путь без изменений.
Следующий этап — **Build 020: безопасное подключение `InstrumentStore` к production-пути с сохранением legacy-совместимости**.