Files
dzentra_bot/docs/migrations/build_060_27_architecture.md

29 KiB
Raw Blame History

Build 060.27 — Persistent Market Data Storage Architecture

Статус: Accepted

Build: 060.27

Подсистема: Market Data Acquisition / Persistent Storage

Дата начала: 2026-07-31

Дата завершения: 2026-08-01

Версия документа: 1.5


1. Назначение

Build 060.27 создаёт долговременное PostgreSQL-хранилище Canonical Market Data. Данные должны сохраняться независимо от времени жизни процесса и стать основой для Persistent Checkpoint, исторических запросов и Replay в следующих Build.

В Build реализуется production-интеграция только для Trades. Для Quotes и Candles создаются схема, repository-контракты и Storage API, но существующие legacy/runtime consumers пока к ним не подключаются.


2. Статус подэтапов

Подэтап Название Статус
060.27.0 Storage Contract and Boundaries Accepted
060.27.1 PostgreSQL Schema, Migrations and Pool Accepted
060.27.2 Persistent Trade Repository Accepted
060.27.3 Quote and Candle Repositories Accepted
060.27.4 Storage API and Retention Accepted
060.27.5 Trade Runtime Persistence Integration Accepted
060.27.6 Bootstrap and Lifecycle Integration Accepted
060.27.7 Integration and Failure Verification Accepted
060.27.8 Final Regression and Acceptance Accepted

060.27.0060.27.1 прошли отдельный read-only review. После исправления cleanup при системном прерывании и удаления двух дублирующих индексов повторный review завершён без findings.

060.27.2 прошёл отдельный read-only review конкурентных вставок, Canonical conflict, provenance, batch atomicity и error propagation. Review завершён без findings.

060.27.3 прошёл отдельный read-only review Quote timestamp collision, Candle revision identity, interval case, concurrent provenance и transaction rollback. Review завершён без findings.

060.27.4 принят после исправления проверки фактического PostgreSQL partition bound и изоляции Retention от concurrent writers. Повторный read-only review concurrent creation, default-row move, DDL safety, точности cutoff и полного rollback завершён без findings.

060.27.5 принят после отдельного read-only review durable-before- checkpoint ordering, duplicate provenance, fatal error propagation, cancellation-safe live offload и единственного владельца записи. Review завершён без findings; целевые тесты и полная регрессия прошли.

060.27.6 реализует disabled-by-default Storage feature flag, общий bootstrap graph и управляемый Application lifecycle. Целевые тесты и полная регрессия прошли. Отдельный read-only review feature flag, отсутствия Storage I/O при composition, partial startup, shutdown order, repeated cancellation и task ownership завершён без findings.

060.27.7 добавляет безопасный opt-in PostgreSQL harness и выполняет реальные schema, repository, partition, retention, Runtime persistence и failure-сценарии на одноразовой локальной базе. Настоящий PostgreSQL выявил ошибку параметризации partition DDL; границы CHECK и ATTACH теперь встраиваются как безопасные psycopg.sql.Literal.

Первичный review выявил обход local-only проверки через PostgreSQL service/неявный endpoint и недостаточное доказательство одновременного участия concurrent callers. Harness теперь запрещает service, неявные и смешанные local/remote endpoints, а непосредственно перед destructive cleanup повторно сверяет точное имя базы и identity контрольного соединения. Migration и partition tests удерживают advisory lock, пока PostgreSQL не подтвердит ожидание обеих caller-сессий; Trade writers синхронизируются общим барьером. Повторный review завершён без findings, подэтап принят.

060.27.8 выполнил финальную verification-only приёмку без изменения production-кода. Целевой unit-набор, новый одноразовый PostgreSQL, десять повторов PostgreSQL integration, общий integration, fixed stress и полная offline-регрессия прошли. Итоговый read-only review не выявил новых findings; подэтап и Build 060.27 приняты.


3. Принятые архитектурные решения

  1. В production runtime этого Build подключается только хранение Trades.
  2. Для Quotes и Candles создаётся storage foundation без изменения существующего legacy/runtime wiring.
  3. Operational checkpoint можно продвигать только после успешного сохранения соответствующего Trade.
  4. Ошибка включённого persistent storage является фатальной: Runtime не должен скрывать потерю истории и продолжать работу как будто всё успешно.
  5. Raw exchange documents в Build 060.27 не сохраняются. Хранилище принимает только прошедшие validation и mapping Canonical Models.
  6. Canonical history по умолчанию не удаляется. Retention включается только явной настройкой и отдельной операцией.
  7. Идентичность Trade задаётся кортежем venue, symbol, trade_id, executed_at. Включение времени защищает от коллизии после полного цикла signed 32-bit Trade ID.
  8. Market Data размещаются в PostgreSQL schema market_data, изменения схемы выполняются упорядоченными SQL-миграциями, соединения выдаёт один явно управляемый pool.

4. Архитектурная граница

Canonical Trade / Quote / Candle
              │
              ▼
market_data.storage contracts
              │
              ▼
PostgreSQL repositories (060.27.2060.27.3)
              │
              ▼
storage.PostgresConnectionPool
              │
              ▼
PostgreSQL schema market_data

Storage зависит от Canonical Models. Canonical Models, Consistency, Recovery и Transport не зависят от PostgreSQL или psycopg.

Trade Runtime использует общий acquisition-side контракт TradeObservationSinkProtocol. Его Storage adapter знает о TradeStorageProtocol, venue и UTC clock, но Consistency и Runtime не знают о PostgreSQL repository или pool:

Live / Recovery
      │
      ▼
shared TradeStreamConsistencyController
      │ validated observation
      ▼
TradeObservationSinkProtocol
      │
      ▼
TradeStorageObservationSink → TradeStorageProtocol

Контракты возвращают явный результат записи:

  • INSERTED — новое событие сохранено;
  • DUPLICATE — полностью совпадающее событие уже существовало;
  • PROVENANCE_UPDATED — Canonical payload совпал, но обновлена информация о наблюдении события.

Несовпадающий payload под тем же ключом не является дубликатом и должен приводить к MarketDataStorageConflictError.


5. Модель хранения

5.1. Trades

Таблица market_data.trades хранит Canonical Trade и технические поля provenance:

identity = venue + symbol + trade_id + executed_at
provenance = source + observation_sources
             + first_observed_at + last_observed_at

source сохраняет первый зафиксированный путь получения Trade. observation_sources накапливает уникальные Canonical source names, например WebSocket и REST Recovery, не превращая различие transport provenance в конфликт биржевого факта.

trade_id ограничен signed 32-bit диапазоном. Это соответствует единому rollover-aware контракту Build 060.26, но база не пытается сортировать события только по числовому ID. Исторический порядок определяется прежде всего executed_at.

5.2. Quotes

Таблица market_data.quotes хранит неизменяемые Canonical snapshots. Ключ снимка: venue, symbol, received_at. Nullable exchange_timestamp сохраняется как часть payload, но не используется как обязательный ключ.

source сохраняет первый зафиксированный путь получения, а observation_sources — уникальные источники того же snapshot. Два разных payload с одним received_at являются явным timestamp conflict; repository не изменяет полученное время и не создаёт скрытый surrogate.

5.3. Candles

Свеча может меняться до закрытия интервала. Поэтому market_data.candle_revisions не перезаписывает строку на месте, а хранит последовательность наблюдавшихся ревизий. Ключ:

venue + symbol + interval + open_time + observed_at

Флаг is_final отделяет промежуточную ревизию от финальной. Изменение содержимого или is_final сохраняется как новая ревизия с новым observed_at. Разный source той же ревизии обновляет только observation_sources. Регистр interval сохраняется, потому что имена рыночных интервалов могут быть чувствительны к регистру.

5.4. Партиционирование и retention

Все три таблицы объявлены как range-partitioned по времени события и имеют default partition. Это даёт безопасную запись до появления автоматического partition manager.

Default partition остаётся страховкой для записи до создания нужного месяца. PostgresMarketDataPartitionManager под transaction-level advisory lock создаёт отдельную месячную таблицу, блокирует default partition, переносит подходящие строки и подключает таблицу как partition в одной транзакции.

Созданные Dzentra partitions регистрируются в market_data.partition_registry. Существующая незарегистрированная таблица, отсутствующая зарегистрированная таблица или detached partition считаются ошибкой конфигурации и не принимаются молча.

Canonical retention по умолчанию не ограничен. PostgresMarketDataRetentionService вообще не обращается к базе при выключенной policy. Включённый явный запуск удаляет целиком только зарегистрированные истёкшие partitions, затем удаляет строки до точного UTC cutoff из оставшихся/default partitions. Все настроенные типы очищаются в одной транзакции.


6. Версионированные миграции

StorageMigrationRunner:

  1. получает соединение через переданный provider;
  2. берёт PostgreSQL transaction-level advisory lock;
  3. создаёт таблицу истории public.storage_schema_migrations;
  4. сверяет уже применённые версии и их неизменяемые имена;
  5. применяет отсутствующие версии строго по порядку;
  6. записывает версию в историю в той же транзакции.

Неизвестная версия в базе или другое имя уже применённой версии считается ошибкой. Миграции не должны молча принимать расхождение между кодом и фактической схемой.

Начальный набор:

Версия Назначение
1 schema market_data
2 partitioned Canonical Trades
3 partitioned Canonical Quotes
4 partitioned immutable Candle revisions
5 multiple Trade observation sources
6 Quote and Candle observation sources
7 managed monthly partition registry

7. Connection Pool lifecycle

PostgresConnectionPool имеет явный lifecycle:

construct → open → borrow/return connections → close

Конструктор не открывает сеть. open() ждёт готовности минимального числа соединений и при ошибке освобождает частично созданный pool. connection() до успешного open() запрещён. close() идемпотентен.

В 060.27.6 pool, migration runner, Trade repository и observation sink собираются без сетевых операций. Application запускает blocking startup через owned asyncio.to_thread task до Telegram и Trade Runtime:

pool.open → migrations.run → Telegram + Trade Runtime

Shutdown выполняется в обратном порядке зависимостей. Сначала полностью останавливается Trade Runtime и завершаются его Storage-вызовы, затем закрывается pool и после этого bot session.


8. Обработка ошибок

  • ошибки lifecycle pool оборачиваются в PostgresConnectionPoolError;
  • ошибки запуска или расхождения миграций оборачиваются в StorageMigrationError;
  • repository validation, conflict и database operations используют иерархию MarketDataStorageError;
  • KeyboardInterrupt и SystemExit не превращаются в storage errors;
  • ошибка включённого persistent storage фатальна и не скрывается;
  • ошибка cleanup не заменяет исходную startup/runtime ошибку;
  • cancellation ждёт уже начатый blocking lifecycle operation.

9. Границы реализации 060.27.0060.27.8

Реализовано:

  • отдельный пакет market_data.storage;
  • write-only Protocol contracts для Trades, Quotes и Candles;
  • результаты одиночной и пакетной идемпотентной записи;
  • отдельная иерархия ошибок Market Data Storage;
  • versioned migration runner с advisory lock;
  • partitioned PostgreSQL schema для трёх Canonical типов;
  • явно открываемый и закрываемый PostgreSQL connection pool;
  • синхронный PostgresTradeRepository без ownership pool lifecycle;
  • одиночная и атомарная пакетная запись Canonical Trades;
  • exact duplicate, provenance update и canonical conflict semantics;
  • стабильный порядок batch-записи и rollback всей пачки при ошибке;
  • синхронные PostgresQuoteRepository и PostgresCandleRepository;
  • immutable Quote snapshot и Candle revision conflict semantics;
  • multiple-source provenance для Trades, Quotes и Candle revisions;
  • общие validation helpers трёх PostgreSQL repositories;
  • единый write-only MarketDataStorage facade без lifecycle ownership;
  • UTC monthly partition descriptors для трёх Canonical типов;
  • транзакционное создание, перенос и attach managed partitions;
  • реестр Dzentra-managed partitions в migration 7;
  • disabled-by-default retention с отдельным явным запуском;
  • удаление полных partitions и точная очистка partial/default данных;
  • общий advisory lock для partition lifecycle и retention;
  • закреплённые версии psycopg и psycopg-pool;
  • общий optional TradeObservationSinkProtocol для Live и Recovery;
  • единственный persistence owner в TradeStreamConsistencyController;
  • durable write до продвижения operational checkpoint;
  • запись корректных дубликатов для обновления provenance;
  • выполнение синхронной live-записи вне asyncio event loop;
  • cancellation-safe ожидание уже начатой live-записи при shutdown;
  • отдельный MarketDataStorageSettings и безопасный feature flag;
  • единый bootstrap graph pool → migrations → Trade repository → sink;
  • использование одного Settings snapshot и существующих DB_*;
  • Storage startup до Telegram и Trade Runtime;
  • partial startup cleanup при migration failure;
  • закрытие pool только после полной остановки Trade Runtime;
  • cancellation-safe Application startup и shutdown tasks.
  • opt-in harness только для явно указанной локальной disposable базы;
  • отсутствие fallback PostgreSQL integration-тестов на application DB_*;
  • real PostgreSQL verification миграций, repositories и транзакций;
  • real concurrent migration, Trade write и partition callers;
  • real partition move, retention cutoff и multi-type rollback;
  • local WebSocket/REST Runtime persistence в настоящий PostgreSQL;
  • real reconnect/recovery provenance через общий Trade repository;
  • fatal persistence failure без ложного продвижения checkpoint;
  • проверка Application ownership pool и отсутствия оставшихся соединений.

Не реализовано на этом шаге:

  • чтение истории;
  • автоматический partition/retention scheduling;
  • подключение legacy Quote/Candle consumers.

10. Критерии приёмки 060.27.0060.27.8

  1. Storage contracts не импортируют Runtime, Consistency или Recovery.
  2. Конструктор pool не выполняет сетевых операций.
  3. Частично открытый pool очищается при ошибке startup.
  4. Повторный close() безопасен.
  5. Миграции выполняются один раз, последовательно и атомарно.
  6. Concurrent migration runners сериализуются advisory lock.
  7. Schema содержит явные idempotency keys и Canonical constraints.
  8. Trade Runtime не импортирует PostgreSQL repository или pool.
  9. Целевые тесты и обычная регрессия проходят успешно.
  10. Источник доставки не является частью Canonical Trade conflict.
  11. Один конфликт или database error откатывает весь Trade batch.
  12. Repository не владеет pool lifecycle и не импортирует Runtime.
  13. Quote timestamp collision и Candle revision conflict не скрываются.
  14. Quote/Candle source обновляет provenance, а не Canonical payload.
  15. Существующие Quote/Candle consumers не подключены к PostgreSQL.
  16. Storage facade делегирует записи и не владеет pool lifecycle.
  17. Имена таблиц и колонок выбираются только из закрытого набора и экранируются через psycopg.sql.Identifier.
  18. Повторное создание месяца идемпотентно, а расхождение реестра и PostgreSQL является явной ошибкой.
  19. Неудачный setup partition откатывает создание, перенос и registry.
  20. Retention требует отдельного enabled и положительного окна.
  21. Выключенный retention не получает соединение и ничего не удаляет.
  22. Ошибка очистки одного типа откатывает всю retention-операцию.
  23. Runtime, Scheduler и существующие consumers не импортируют новый Storage API.
  24. Live и Recovery используют один persistence sink через общий TradeStreamConsistencyController.
  25. Validated Trade сохраняется до изменения checkpoint и dedup window.
  26. Корректный дубликат сохраняется для provenance без продвижения checkpoint.
  27. Ordering error, conflicting duplicate и invalid Trade не передаются в Storage.
  28. Ошибка Storage не скрывается и не продвигает checkpoint.
  29. Live persistence выполняется через owned asyncio.to_thread task при закрытом live-processing gate.
  30. Shutdown ждёт уже начатую live-запись и не оставляет её фоновой.
  31. Отсутствующий optional sink сохраняет прежнее поведение Composition.
  32. Bootstrap/settings не изменены, PostgreSQL pool не открывается.
  33. Целевой набор и полная обычная регрессия проходят успешно.
  34. Storage feature flag выключен по умолчанию и не строит зависимости.
  35. Включённый Storage требует включённый Trade Stream.
  36. Pool settings валидируются только при включённом Storage.
  37. Bootstrap graph не открывает PostgreSQL и не запускает migrations.
  38. Migration runner и Trade repository используют один pool identity.
  39. Один observation sink передаётся общим Live/Recovery controller.
  40. Pool открывается и migrations завершаются до root Runtime tasks.
  41. Migration failure закрывает открытый pool и не запускает Runtime.
  42. Blocking startup/shutdown выполняются вне asyncio event loop.
  43. Cancellation ждёт завершения уже начатой blocking операции.
  44. Trade Runtime полностью остановлен до закрытия pool.
  45. Storage cleanup error не заменяет исходную Runtime ошибку.
  46. Retention и partition scheduling автоматически не запускаются.
  47. PostgreSQL integration требует точный opt-in flag и отдельный DSN.
  48. Integration DSN принимает только localhost/local socket и database name с префиксом dzentra_test_.
  49. Harness не читает application DB_* и не управляет Docker скрытно.
  50. Migrations 17 реально применяются в PostgreSQL и повторный запуск ничего не меняет.
  51. Два real migration runners применяют каждую версию ровно один раз.
  52. Trades, Quotes и Candle revisions проходят real insert, duplicate, provenance и conflict paths.
  53. Trade batch реально откатывает предшествующую вставку при конфликте.
  54. Два concurrent Trade writers сходятся к одной Canonical строке.
  55. Partition DDL использует безопасные SQL literals вместо запрещённых PostgreSQL parameters в ALTER TABLE.
  56. Real partition manager переносит default rows и сериализует callers.
  57. Real retention соблюдает точный cutoff и общий transaction rollback.
  58. Live и reconnect/recovery Runtime сохраняют Trades в один repository.
  59. Закрытый pool приводит к fatal Runtime error без продвижения checkpoint и без записи проблемного Trade.
  60. Application открывает и мигрирует Storage до Runtime, а закрывает pool после Runtime; после shutdown нет owned tasks и pool connections.
  61. Выключенный opt-in PostgreSQL-набор не требует локальной базы и корректно пропускается обычной regression-командой.
  62. Destructive cleanup непосредственно перед DDL повторно проверяет точное имя disposable базы и специальный control application name.
  63. Concurrent migration и partition tests подтверждают в PostgreSQL, что обе caller-сессии действительно ожидают один advisory lock.

10.1. Opt-in PostgreSQL verification

Тесты никогда не используют production DB_*. Первый запуск требует явного одноразового PostgreSQL и выполняется командой вида:

DZENTRA_RUN_POSTGRES_TESTS=1 \
DZENTRA_TEST_POSTGRES_DSN=postgresql://test:test@127.0.0.1:55432/dzentra_test_060_27 \
.venv/bin/python -m pytest -q -m integration \
tests/integration/market_data/storage

Harness проверяет имя базы и каждый local endpoint до соединения, запрещает service/implicit fallback, берёт session advisory lock и очищает только market_data и собственную таблицу migration history. Перед каждым destructive cleanup он повторно подтверждает фактическое имя disposable базы и identity контрольного соединения.


11. Финальная приёмка 060.27.8

Финальный этап выполнялся по verification-only правилу: production-код не изменялся, потому что проверки не выявили нового дефекта.

Результаты на 2026-08-01:

Targeted Storage/Runtime unit suite:       307 passed
PostgreSQL control run:                     15 passed
PostgreSQL repeated ten times:             150 passed
Combined integration suite:                 26 passed
Fixed stress:                                3 passed, 1 deselected
PostgreSQL suite without opt-in:            15 skipped
Full offline regression:                  2119 passed, 31 deselected
git diff --check:                           clean
Untracked files diff check:                 clean

Во всех integration и stress проверках ResourceWarning считался ошибкой. Одноразовый PostgreSQL 16 использовал временное tmpfs хранилище и после проверки был остановлен и удалён.

Итоговый read-only review повторно проверил архитектурные зависимости, SQL/DDL safety, durable-before-checkpoint ordering, fatal persistence failure, lifecycle pool и Runtime, opt-in isolation и test coverage. Новых findings не обнаружено.

12. Следующий Build

Build 060.27 завершён и принят. Следующий этап — Build 060.28, Persistent Checkpoint and Startup Recovery.