14 KiB
Build 019 — Подготовка переноса кэша в Storage
Статус
COMPLETE
Цель
Подготовить независимый storage-контракт для хранения канонического справочника инструментов перед последующим переносом legacy-кэша:
ExchangeService._exchange_symbols_cache
из слоя:
src/integrations/exchange
в слой:
src/storage
без изменения текущего production-пути и без нарушения работы существующего бота.
Архитектурный принцип
До Build 019 кэш справочника торговых инструментов находился непосредственно внутри legacy-интеграционного сервиса:
ExchangeService._exchange_symbols_cache
Это создаёт архитектурную связь между:
- получением Instrument Reference Data;
- legacy-моделью
ExchangeSymbol; - интеграционным слоем биржи;
- runtime-хранением загруженного справочника.
В новой архитектуре ответственность разделяется:
Market Data Acquisition
│
▼
tuple[Instrument, ...]
│
▼
Storage
│
▼
Compatibility Layer
│
▼
Legacy ExchangeSymbol
Build 019 создаёт только новый storage-контракт и его in-memory реализацию.
Переключение production-кода на новый store в рамках Build 019 не выполняется.
Созданные файлы
app/src/storage/exceptions.py
app/src/storage/instrument_store.py
app/tests/unit/storage/test_instrument_store.py
src/storage/exceptions.py
Создана базовая иерархия ошибок storage-слоя:
Exception
│
▼
StorageError
│
▼
InstrumentStoreError
StorageError
Базовая ошибка storage-слоя.
InstrumentStoreError
Специализированная ошибка операций и нарушений контракта хранилища справочника инструментов.
src/storage/instrument_store.py
Созданы:
InstrumentStoreProtocol
InMemoryInstrumentStore
InstrumentStoreProtocol
Определяет независимый контракт runtime-хранилища канонического справочника инструментов.
Поддерживаемые операции:
get(
source_name: str,
) -> tuple[Instrument, ...] | None
set(
source_name: str,
instruments: tuple[Instrument, ...],
) -> None
clear(
source_name: str | None = None,
) -> None
Контракт не зависит от:
ExchangeService;ExchangeSymbol;- Dzengi REST API;
- PostgreSQL;
- Redis;
- Telegram UI;
- legacy compatibility mapper.
Семантика get()
Метод:
get(source_name)
возвращает:
None
если данные для источника никогда не сохранялись.
Это означает:
cache miss
Если в store был успешно сохранён пустой справочник:
()
метод возвращает именно:
()
Таким образом:
None != ()
и состояния:
данные отсутствуют
и:
успешно загружен пустой справочник
не смешиваются.
Семантика set()
Метод принимает исключительно:
tuple[Instrument, ...]
Это сохраняет immutable-контракт новой модели Instrument Reference Data.
Store не выполняет:
- копирование tuple;
- сортировку;
- преобразование элементов;
- mapping в
ExchangeSymbol; - нормализацию
Instrument; - изменение порядка элементов.
Сохраняется исходный объект tuple.
Следовательно:
store.set("dzengi", instruments)
assert store.get("dzengi") is instruments
Семантика clear()
Поддерживаются два режима.
Очистка конкретного источника:
store.clear("dzengi")
Полная очистка store:
store.clear()
Очистка неизвестного источника является идемпотентной и не вызывает ошибку.
Изоляция источников
Store поддерживает независимое хранение нескольких источников:
dzengi
secondary
other-source
Например:
InMemoryInstrumentStore
├── dzengi
│ └── tuple[Instrument, ...]
│
└── secondary
└── tuple[Instrument, ...]
Изменение или очистка одного источника не влияет на остальные.
Нормализация имени источника
Внешние пробелы удаляются:
" dzengi "
нормализуется в:
"dzengi"
Регистр сохраняется.
Следовательно:
dzengi
и:
DZENGI
являются разными ключами.
Пустые имена источников запрещены:
""
" "
" "
"\t"
"\n"
и приводят к:
InstrumentStoreError
Runtime-валидация
InMemoryInstrumentStore.set() проверяет:
- что набор передан как
tuple; - что каждый элемент является экземпляром
Instrument.
Нарушение контракта приводит к:
InstrumentStoreError
Что намеренно не изменялось
Build 019 не изменяет:
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-кэш остаётся на прежнем месте:
class ExchangeService:
_exchange_symbols_cache: list[ExchangeSymbol] | None = None
Текущий production-путь остаётся неизменным:
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-путь пока не подключён.
Тестовое покрытие
Создан файл:
app/tests/unit/storage/test_instrument_store.py
Проверены:
- соответствие
InstrumentStoreProtocol; - cache miss;
- сохранение и получение данных;
- сохранение identity исходного tuple;
- различие между
Noneи пустым tuple; - изоляция разных источников;
- очистка одного источника;
- полная очистка store;
- замена ранее сохранённого значения;
- нормализация внешних пробелов имени источника;
- сохранение регистра имени источника;
- отклонение пустых имён источников;
- отклонение списка вместо tuple;
- отклонение объектов, не являющихся
Instrument; - сохранение порядка инструментов;
- отсутствие изменения входного tuple;
- изоляция разных экземпляров store;
- идемпотентная очистка неизвестного источника;
- наследование
InstrumentStoreErrorотStorageError.
Результаты проверок
Unit-тесты нового store
Команда:
python -m pytest \
tests/unit/storage/test_instrument_store.py \
-q
Результат:
32 passed in 0.02s
Проверка компиляции
Команда:
python -m py_compile \
src/storage/exceptions.py \
src/storage/instrument_store.py \
tests/unit/storage/test_instrument_store.py
Результат:
успешно
Полный regression suite
Команда:
python -m pytest -q
Результат:
419 passed in 0.21s
Архитектурная проверка
Выполнена команда:
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
External Exchange API
│
▼
Market Data Acquisition
│
▼
tuple[Instrument, ...]
│
├──────────────────────────────┐
│ │
▼ ▼
InMemoryInstrumentStore Compatibility Layer
│ │
│ ▼
│ list[ExchangeSymbol]
│ │
│ ▼
│ ExchangeService._exchange_symbols_cache
│
▼
готов к будущему подключению
в production-путь
На текущем этапе новый store существует независимо от legacy-кэша.
Граница Build 019
Build 019 считается завершённым, потому что:
- создан независимый storage-контракт для
Instrument; - создана in-memory реализация store;
- сохранена семантика immutable
tuple[Instrument, ...]; - определено различие между cache miss и пустым справочником;
- обеспечена изоляция источников;
- добавлена специализированная иерархия storage-ошибок;
- production-код не изменён;
- legacy-кэш не удалён;
- полный regression suite проходит успешно.
Следующий шаг
Следующий логический этап:
Build 020 — Подключение Instrument Store к production-пути
Цель следующего этапа:
переключить хранение канонического tuple[Instrument, ...]
с legacy-кэша ExchangeService
на InMemoryInstrumentStore
при сохранении внешнего legacy-контракта:
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-совместимости.