Files
dzentra_bot/docs/migrations/build_019.md

14 KiB
Raw Blame History

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() проверяет:

  1. что набор передан как tuple;
  2. что каждый элемент является экземпляром 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 считается завершённым, потому что:

  1. создан независимый storage-контракт для Instrument;
  2. создана in-memory реализация store;
  3. сохранена семантика immutable tuple[Instrument, ...];
  4. определено различие между cache miss и пустым справочником;
  5. обеспечена изоляция источников;
  6. добавлена специализированная иерархия storage-ошибок;
  7. production-код не изменён;
  8. legacy-кэш не удалён;
  9. полный 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-совместимости.