Files
dzentra_bot/docs/operations/trades_feed_runtime.md

51 KiB
Raw Blame History

Trade Stream Production Runtime — эксплуатационное руководство

Статус: Current; accepted in Build 060.30.2

Область: Trades Feed, persistent Market Data Storage и связанные проверки

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


Связанные документы


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

Документ описывает настройку, запуск, остановку и проверку Production Trades Feed. Он не заменяет процедуру deployment конкретной среды и не содержит production credentials.

Главные правила:

  1. PostgreSQL нужен обычному запуску приложения даже при выключенном Market Data Storage: Bootstrap всегда создаёт базовые таблицы журнала и balance snapshots.
  2. Trade Stream и persistent Storage выключены по умолчанию.
  3. Storage разрешён только вместе с Trade Stream.
  4. Persistent checkpoint включается вместе со Storage; отдельного флага нет.
  5. Ошибка включённого Trade Runtime, Storage или Recovery фатальна.
  6. Retention, monthly partition creation и Replay не запускаются автоматически.
  7. Docker Compose использует hardened baseline из ADR-060.30-008; старые images, собранные до появления .dockerignore, остаются потенциально скомпрометированными и не должны публиковаться.

2. Проверенная среда и version policy

Компонент Статус
Python 3.12.13 Закреплённый Production Docker base; Pyright target — 3.12
PostgreSQL 16.14 Полная Storage/Restart/Replay acceptance выполнена
PostgreSQL 17.10 Application compatibility проверена в 060.30.7; текущим Compose не используется и deployment baseline не является
psycopg 3.2.9 Закреплён в production requirements
psycopg-pool 3.3.1 Закреплён в production requirements
websockets 13.1 Закреплён в production requirements

PostgreSQL 16.14 остаётся единственным поддерживаемым production deployment baseline. Изолированная matrix 060.30.7 подтвердила совместимость приложения с PostgreSQL 17.10, включая migrations, repositories, checkpoint, Historical Access, Replay, Trade Runtime integration и restart.

Эта проверка не принимает hardened PostgreSQL 17 image, Compose cutover или upgrade существующего volume. Production deployment на PostgreSQL 17 требует отдельного infrastructure gate с повторной проверкой role bootstrap, healthcheck, image и Compose path.

Для первого production deployment допустимы два безопасных пути:

  1. использовать текущий проверенный hardened PostgreSQL 16.14 image, закреплённый одновременно tag и digest;
  2. переходить на PostgreSQL 17 только после отдельной приёмки точного hardened deployment image и полного Compose path.

Нельзя переключать major version на существующем data volume. Upgrade требует проверенного pg_dump/restore либо отдельной процедуры pg_upgrade.


3. Подготовка Python environment

Команды выполняются из корня repository:

python3.12 -m venv app/.venv
app/.venv/bin/python -m pip install --upgrade pip
app/.venv/bin/python -m pip install -r app/requirements-dev.txt

app/requirements.txt остаётся читаемым списком прямых production dependencies. Production image устанавливает полный транзитивный app/requirements.lock только с pip --require-hashes. requirements-dev.txt включает прямые production requirements и инструменты pytest/Pyright для developer environment.

Lock регенерируется намеренно и review-ится как dependency update:

uv pip compile app/requirements.txt \
  --generate-hashes \
  --universal \
  --python-version 3.12 \
  --no-header \
  --no-annotate \
  --output-file app/requirements.lock

После генерации сохраняются только три русскоязычные служебные строки в начале файла; dependency records и hashes остаются машинными данными.

Нельзя вручную убирать hash или обновлять только транзитивную version без повторных build, smoke, pip check и regression.

Создайте app/.env на основе app/.env.example, но не перезаписывайте существующий файл автоматически и никогда не добавляйте его в Git.


4. Контракт переменных окружения

4.1. Application и Telegram

Переменная Default Назначение
BOT_TOKEN нет Прямой token; взаимоисключающий с BOT_TOKEN_FILE
BOT_TOKEN_FILE нет UTF-8 файл обязательного Telegram token
BOT_PARSE_MODE HTML Режим форматирования Telegram
APP_ENV dev Имя среды
LOG_LEVEL INFO Уровень стандартного logging
TZ Europe/Minsk Часовой пояс общих компонентов
DEBUG_ENABLED false Общий debug-флаг
JOURNAL_DEBUG_ENABLED false Debug-события журнала
DZENTRA_RUNTIME_ENV_FILE app/.env Отдельный writable env-файл для runtime-переключателей

DEBUG_ENABLED и JOURNAL_DEBUG_ENABLED перечислены в .env.example и по умолчанию выключены.

Если DZENTRA_RUNTIME_ENV_FILE присутствует, его значение обязано быть непустым. Явно пустая строка считается ошибкой, а не просьбой молча вернуться к read-only app/.env внутри container.

4.2. PostgreSQL

Переменная Default Назначение
DB_HOST localhost Hostname PostgreSQL
DB_PORT 5432 PostgreSQL port
DB_NAME dzentra_bot Database name
DB_USER dzentra_bot Database role
DB_PASSWORD пусто Database password
DB_PASSWORD_FILE нет UTF-8 файл database password вместо прямого значения

Для запуска приложения на host против опубликованного Compose port:

DB_HOST=127.0.0.1
DB_PORT=5432

Для bot container внутри одной Compose network:

DB_HOST=postgres
DB_PORT=5432

localhost внутри bot container означает сам bot container, а не PostgreSQL service.

Текущий legacy DSN builder корректно обрабатывает не все специальные символы пароля. До перехода этого пути на make_conninfo используйте длинный случайный URL-safe пароль из букв, цифр, - и _. Значения DB_NAME и POSTGRES_DB обозначают одну базу. DB_USER и DB_PASSWORD относятся к отдельной непривилегированной роли приложения; они не должны совпадать с POSTGRES_ADMIN_USER и паролем bootstrap-admin.

Изменение файлов с admin- или application-паролем после инициализации named volume не меняет пароли уже существующих PostgreSQL roles. Для ротации нужна отдельная контролируемая DBA-процедура.

4.3. Exchange

Переменная Default Назначение
EXCHANGE_ENABLED false Legacy exchange feature flag
EXCHANGE_NAME dzengi Canonical venue для Storage
EXCHANGE_BASE_URL пусто REST base URL, обязательный для Recovery
EXCHANGE_WS_URL пусто Legacy WebSocket consumers, не Trade Runtime fallback
EXCHANGE_API_KEY пусто API key, если нужен endpoint
EXCHANGE_API_KEY_FILE нет UTF-8 файл API key вместо прямого значения
EXCHANGE_API_SECRET пусто API secret других exchange operations
EXCHANGE_API_SECRET_FILE нет UTF-8 файл API secret вместо прямого значения
EXCHANGE_TIMEOUT_SEC 10 REST timeout
EXCHANGE_TESTNET false Признак тестовой среды
DEFAULT_SYMBOL ETH/USD_LEVERAGE Общий symbol приложения

Production Trades Feed использует отдельный TRADE_STREAM_WS_URL. EXCHANGE_WS_URL не подставляется вместо него.

Для каждого из четырёх secrets разрешён ровно один источник: прямая переменная либо соответствующая *_FILE. Одновременное задание двух источников, пустой путь, unreadable/не-UTF-8 файл и пустое содержимое завершают startup ошибкой. Из file value удаляется ровно один конечный перевод строки; остальное содержимое не нормализуется. Текст ошибки не содержит значение секрета.

4.4. Trade Stream

Переменная Default Правило
TRADE_STREAM_ENABLED false Явно включает Runtime
TRADE_STREAM_WS_URL пусто Обязателен при включении
TRADE_STREAM_SYMBOLS пусто Обязательный comma-separated список
TRADE_STREAM_OPEN_TIMEOUT_SECONDS 10 Положительное конечное число
TRADE_STREAM_PROBE_TIMEOUT_SECONDS 20 Ping/Pong timeout
TRADE_STREAM_CLOSE_TIMEOUT_SECONDS 10 Close timeout
TRADE_STREAM_HEARTBEAT_TIMEOUT_SECONDS 30 Максимальное отсутствие activity
TRADE_STREAM_SCHEDULER_INTERVAL_SECONDS 5 Интервал liveness checks
TRADE_STREAM_RECOVERY_WINDOW_MS 3599999 Максимальное REST recovery window
TRADE_STREAM_SUBSCRIPTION_ACK_TIMEOUT_SECONDS 10 Startup ACK timeout
TRADE_STREAM_STARTUP_MARKET_BUFFER_CAPACITY 10000 Ранний FIFO до ACK/Recovery

Symbols нормализуются в uppercase, сортируются и удаляют дубликаты. Пустые элементы запрещены. Все числовые параметры должны быть строго положительными; float-параметры также должны быть конечными.

Built-in keepalive библиотеки websockets отключён. Liveness проверяет собственный Ping/Pong probe через Scheduler и Heartbeat.

4.5. Market Data Storage

Переменная Default Правило
MARKET_DATA_STORAGE_ENABLED false Persistent Trades и checkpoint
MARKET_DATA_STORAGE_POOL_MIN_SIZE 1 Положительное целое
MARKET_DATA_STORAGE_POOL_MAX_SIZE 4 Не меньше min size
MARKET_DATA_STORAGE_POOL_TIMEOUT_SECONDS 10 Положительное конечное число

MARKET_DATA_STORAGE_ENABLED=true при выключенном Trade Stream отклоняется во время загрузки settings.


5. Режимы запуска

5.1. Application без Trade Stream

TRADE_STREAM_ENABLED=false
MARKET_DATA_STORAGE_ENABLED=false

Trade Runtime и отдельный Market Data pool не создаются. PostgreSQL всё равно должен быть доступен для legacy init_schema() и журнала.

5.2. Volatile Trade Runtime

TRADE_STREAM_ENABLED=true
MARKET_DATA_STORAGE_ENABLED=false

Доступны live processing и reconnect REST Recovery. Consistency state и checkpoint живут только в памяти; после restart durable continuation нет. Startup не выполняет persistent Hydration и Startup Recovery.

5.3. Durable Trades

TRADE_STREAM_ENABLED=true
MARKET_DATA_STORAGE_ENABLED=true

Приложение открывает отдельный pool, применяет migrations, сохраняет Trades и persistent checkpoint, выполняет Hydration и Startup Recovery.

5.4. Host startup

Из каталога app:

.venv/bin/python -m src.main

Ошибка включённого Trade Runtime или его неожиданное нормальное завершение останавливает всё приложение, а не оставляет Telegram polling в частично работающем режиме.


6. Startup и graceful shutdown

6.1. Application startup

load и validate settings
→ configure logging
→ legacy init_schema() через PostgreSQL
→ собрать Storage/Runtime dependency graphs без I/O
→ создать Bot и Dispatcher
→ открыть Market Data pool, если Storage включён
→ применить migrations 19
→ запустить Telegram polling и root Trade Runtime task

6.2. Persistent Trade Runtime startup

hydrate checkpoint и deduplication tail
→ WebSocket connect
→ subscribe
→ дождаться matching ACK, буферизуя ранние market documents
→ REST Recovery при закрытом Live gate
→ обработать buffered FIFO
→ запустить Supervisor, receive loop и Scheduler

Corrupt/orphan checkpoint, ACK timeout, overflow и Recovery failure фатальны. Тихого сброса persistent state нет.

6.3. Reconnect

transport error или Heartbeat timeout
→ одна generation-aware single-flight операция
→ закрыть Live gate
→ Disconnect
→ Connect
→ restore subscriptions
→ REST Recovery
→ открыть Live gate

Reconnect выполняет одну попытку без retry/backoff loop. Restore subscriptions не включает отдельное ожидание ACK. Ошибка терминальна до внешнего контролируемого restart процесса.

6.4. Graceful shutdown

отменить Telegram polling
→ runtime.stop()
→ завершить startup worker, если он уже начался
→ остановить Scheduler
→ остановить Supervisor
→ отменить и дождаться receive loop
→ остановить WebSocket Session
→ очистить subscriptions
→ дождаться root Runtime task
→ закрыть Market Data pool
→ закрыть bot HTTP session

Для externally managed service нельзя использовать универсальный 60-секундный kill deadline. Бюджет graceful shutdown должен покрывать:

  • уже начатые blocking Storage/migration operations;
  • все запланированные Recovery windows для всех symbols с учётом EXCHANGE_TIMEOUT_SEC;
  • WebSocket close и дополнительный измеренный cleanup margin.

Recovery gap заранее не ограничен, поэтому production timeout нужно рассчитать по принятому максимальному gap или контролировать завершение операции по logs. Migration 9 выполняется только в отдельном maintenance window без короткого внешнего SIGKILL deadline. Повторная cancellation не должна заменять ожидание уже начатой durable operation.

Bot image использует SIGINT как container stop signal. До запуска Aiogram polling его обрабатывает asyncio.run: корневая задача получает cancellation и дожидается уже начатой blocking lifecycle operation. SIGTERM для этого раннего startup-окна не применяется, потому что обработчик Aiogram в этот момент ещё не установлен.


7. Docker Compose: hardened baseline

ADR-060.30-008 заменил development skeleton на контролируемый baseline для одного Production instance. Это не готовая HA-платформа: у bot пока нет HTTP readiness/health endpoint, resource policy, backup automation, metrics и alerting. Эти ограничения нельзя компенсировать бесконечным автоматическим restart.

Защитные свойства текущей конфигурации:

  • корневой .dockerignore является allowlist: в context входят только Dockerfiles, PostgreSQL hardening scripts, production requirements и app/src;
  • Python 3.12.13 и PostgreSQL 16.14 закреплены tag + digest;
  • все 25 production packages закреплены полным hash-lock, а pip запускается с --require-hashes;
  • bot работает как UID/GID 10001:10001, оба services имеют read-only root filesystem, no-new-privileges и сброшенные capabilities;
  • PostgreSQL получает только необходимые entrypoint capabilities и не публикует port на host;
  • database network изолирована, а внешний network подключён только bot;
  • реальные token/password не передаются через YAML или env_file;
  • fatal exit не превращается в бесконечный restart loop;
  • обязательный project namespace изолирует volumes и networks разных окружений и рабочих копий;
  • bot и PostgreSQL shutdown budgets обязательны и не имеют опасного универсального default;
  • изменяемый JOURNAL_DEBUG_ENABLED хранится в отдельном named volume, а не в read-only image filesystem.

7.1. Secret files и Compose controls

Secret manager или оператор создаёт три файла вне repository:

/absolute/protected/dzentra/bot_token
/absolute/protected/dzentra/postgres_admin_password
/absolute/protected/dzentra/db_password

postgres_admin_password используется только bootstrap/admin role, а db_password — отдельной непривилегированной application-role. Значения должны различаться. POSTGRES_ADMIN_USER и DB_USER также должны быть разными; defaults — dzentra_admin и dzentra_bot.

Каталог с secrets должен принадлежать deployment owner и иметь mode 0700. Обычный Linux Compose монтирует local file: без преобразования UID/GID, а bot и PostgreSQL работают под разными UID. Поэтому обычные файлы внутри защищённого каталога должны иметь mode 0444: доступ на host ограничивает каталог, а в каждый container монтируются только нужные ему файлы. Owner-only mode 0600 без отдельного UID remapping непереносим и может сорвать startup на Linux. Не вводите значения secrets в shell command line и не сохраняйте их в app/.env, Compose YAML или control-файле.

Перед каждой Compose-командой задаются только пути и budgets:

export DZENTRA_COMPOSE_PROJECT_NAME=dzentra-production
export BOT_TOKEN_SECRET_FILE=/absolute/protected/dzentra/bot_token
export POSTGRES_ADMIN_PASSWORD_SECRET_FILE=/absolute/protected/dzentra/postgres_admin_password
export DB_PASSWORD_SECRET_FILE=/absolute/protected/dzentra/db_password
export BOT_STOP_GRACE_PERIOD=30m
export POSTGRES_STOP_GRACE_PERIOD=60s

Имя проекта должно быть стабильным для одного окружения и различаться у Production, staging и каждой локальной проверки. Не используйте default compose: смена имени выбирает другой набор named volumes, а повторное использование имени из другой копии подключает её состояние.

30m и 60s являются только примерами формата. Фактические budgets нужно рассчитать по правилам раздела 6.4. Пропущенный путь к secret либо любой stop budget останавливает Compose ещё на этапе interpolation.

Если controls хранятся в файле, этот файл должен находиться вне repository, иметь mode 0600 и содержать только пути/несекретные настройки. Его передают явно через --env-file; автоматический repository .env для production deployment не используется.

Проверка конфигурации не должна печатать развёрнутую модель:

(
  set -eu
  compose_stderr="$(mktemp)"
  trap 'rm -f "$compose_stderr"' EXIT
  test -r "$BOT_TOKEN_SECRET_FILE"
  test -r "$POSTGRES_ADMIN_PASSWORD_SECRET_FILE"
  test -r "$DB_PASSWORD_SECRET_FILE"
  docker compose -f infra/compose/docker-compose.yml \
    config --quiet 2>"$compose_stderr"
  test ! -s "$compose_stderr"
)

Base Compose монтирует Telegram token, bootstrap-admin password и отдельный application database password. EXCHANGE_API_KEY_FILE/EXCHANGE_API_SECRET_FILE поддерживаются Application contract. Когда authenticated exchange operations действительно нужны, используется явный override:

export EXCHANGE_API_KEY_SOURCE_FILE=/absolute/protected/dzentra/exchange_api_key
export EXCHANGE_API_SECRET_SOURCE_FILE=/absolute/protected/dzentra/exchange_api_secret
docker compose \
  -f infra/compose/docker-compose.yml \
  -f infra/compose/docker-compose.exchange-auth.yml \
  config --quiet

Без override эти optional credentials в container не монтируются.

7.2. Build и controlled rollout

Рекомендуемая последовательность первого rollout:

docker compose -f infra/compose/docker-compose.yml build --pull postgres bot
docker compose -f infra/compose/docker-compose.yml up --detach postgres
docker compose -f infra/compose/docker-compose.yml up --detach bot

После запуска порядок проверки такой:

PostgreSQL healthy
→ backup и точная версия подтверждены
→ запущен ровно один bot instance
→ startup/migration logs не содержат fatal error
→ migrations 19 и checkpoints проверены

Оба services имеют pull_policy: build: Compose собирает локальные derived images только из закреплённых base images и allowlist context, а не подменяет их одноимённым remote image.

Внутри Compose DB_HOST всегда равен postgres. PostgreSQL доступен только services в internal network. Для host admin access используйте отдельный временный loopback-only override либо docker compose exec, а не постоянный public port.

Fresh volume создаёт bootstrap-admin и отдельную application-role, передаёт последней ownership базы и снимает опасные role flags. На каждом startup healthcheck проверяет, что application-role существует, не является superuser/role creator/database creator/replication/RLS bypass, не состоит в других ролях и остаётся владельцем базы. Настройка ролей выполняется одной транзакцией; поздняя ошибка откатывает все её изменения, а незавершённый fresh volume остаётся unhealthy и требует проверяемого пересоздания, а не ручного продолжения наполовину выполненного setup. Legacy volume, где прежний dzentra_bot был bootstrap-superuser, намеренно останется unhealthy. Его нельзя «исправлять» автоматическим ALTER ROLE: сначала нужен backup, затем проверенный restore в новый hardened volume либо отдельная DBA-процедура миграции владельца и admin-role.

Старый Compose мог создать compose_dzentra_postgres_data на PostgreSQL 17. PostgreSQL 16.14 не должен открывать этот data directory: in-place downgrade major version запрещён. Такой volume и работающий PostgreSQL 17 не останавливаются и не изменяются hardening-проверкой. Сначала нужны backup и точная проверка версии; production cutover безопаснее отложить до отдельной приёмки hardened PostgreSQL 17 после 060.30.7. Любой logical restore в другую major version выполняется только в новом project/volume после отдельной проверки совместимости и восстановления.

docker compose down сохраняет named volumes. down --volumes удаляет database и runtime state и запрещён без отдельного подтверждения точных targets, проверенного backup и принятой процедуры уничтожения данных.

7.3. Если image уже был собран без .dockerignore

Любой image, собранный при существующем app/.env и отсутствии проверенного .dockerignore, считается потенциально содержащим secrets в слоях, даже если текущий container их не показывает.

До продолжения rollout необходимо:

  1. запретить push и deployment такого image;
  2. сменить BOT_TOKEN, database password, API keys и другие credentials, которые находились в build context;
  3. удалить соответствующие local images/build cache и registry artifacts по принятой инфраструктурной процедуре;
  4. проверить registry и deployment audit logs;
  5. после hardening собрать новый image из чистого context.

Позднее добавление .dockerignore не удаляет secrets из старых layers.


8. Storage migrations

При включённом Market Data Storage migrations запускаются автоматически до Trade Runtime. StorageMigrationRunner:

  • берёт PostgreSQL transaction advisory lock;
  • сверяет migration history как непрерывный prefix;
  • отклоняет неизвестную version и name mismatch;
  • применяет pending migrations одной транзакцией;
  • записывает результат в public.storage_schema_migrations.

Migration table нельзя исправлять вручную.

Проверка состояния:

SELECT version, name, applied_at
FROM public.storage_schema_migrations
ORDER BY version;

Ожидается непрерывная последовательность versions 19.

8.1. Migration 9

Migration 9 является блокирующей. Она:

  • берёт общий partition advisory lock;
  • получает ACCESS EXCLUSIVE locks на Trades, Quotes и Candle revisions;
  • назначает существующим строкам общий replay_sequence;
  • добавляет defaults, NOT NULL, checks, triggers и indexes.

Длительность, WAL, temporary files и требуемое место растут вместе со всем уже накопленным Market Data объёмом.

Для нетривиальной базы обязательно:

  1. остановить все writers и bot instances;
  2. создать и проверить backup;
  3. измерить migration на копии сопоставимого объёма и той же PostgreSQL major version;
  4. проверить свободное место и допустимое maintenance window;
  5. запустить ровно один Application instance;
  6. наблюдать locks и logs до commit;
  7. проверить migrations, row counts и checkpoints после startup.

Online/chunked replacement migration 9 не реализован. Transaction failure откатывает migration, но Application startup остаётся fatal.

8.2. Backup для Compose PostgreSQL

Bot должен быть остановлен, PostgreSQL — оставаться запущенным:

Создайте защищённый каталог вне repository и ограничьте к нему доступ. В примере /absolute/protected/backup необходимо заменить на реальный внешний путь. Команды выполняются в отдельной subshell: строгий umask защищает новый файл, а любой неуспешный шаг завершает процедуру.

(
  set -eu
  umask 077
  backup_dir=/absolute/protected/backup
  mkdir -p "$backup_dir"
  chmod 700 "$backup_dir"

  backup_stamp="$(date -u +%Y%m%dT%H%M%SZ)"
  backup_file="$backup_dir/dzentra_before_migration_9_${backup_stamp}.dump"
  backup_tmp="$(mktemp "$backup_dir/.dzentra_before_migration_9.tmp.XXXXXX")"
  trap 'rm -f "$backup_tmp"' EXIT
  trap 'exit 130' HUP INT TERM
  test ! -e "$backup_file"

  docker compose -f infra/compose/docker-compose.yml exec -T postgres \
    sh -eu -c \
    'exec pg_dump -U "$APP_DB_USER" -d "$POSTGRES_DB" --format=custom --no-owner --no-acl' \
    > "$backup_tmp"
  test -s "$backup_tmp"
  chmod 600 "$backup_tmp"
  docker compose -f infra/compose/docker-compose.yml exec -T postgres \
    pg_restore --list < "$backup_tmp" > /dev/null

  mv -n "$backup_tmp" "$backup_file"
  test ! -e "$backup_tmp"
  test -s "$backup_file"
  trap - EXIT HUP INT TERM
)

APP_DB_USER и POSTGRES_DB читаются внутри PostgreSQL container и соответствуют фактическим DB_USER и DB_NAME текущего Compose окружения. Поэтому backup не зависит от default-значений dzentra_bot.

Временный файл находится на том же filesystem, что и итоговый dump. Поэтому mv -n публикует проверенную копию атомарно и не перезаписывает существующий backup; если перенос не состоялся, проверка оставшегося temporary file завершает процедуру ошибкой.

Проверка списка объектов не заменяет rehearsal restore. Восстановление нужно заранее проверить в отдельной пустой базе или volume. Dump с --no-owner --no-acl не создаёт cluster roles: нужную role и database создают отдельно из secret-management процедуры. Если ownership и roles нужно сохранять, защищённый pg_dumpall --globals-only формируют и проверяют отдельно.

Backup должен иметь согласованные encryption, retention и off-host copy. После процедуры проверьте размер, checksum и тестовый restore. Named volume является persistence, но не резервной копией.


9. Partitions и Retention

Parent tables имеют default partitions, поэтому запись работает без предварительного создания monthly partitions.

PostgresMarketDataPartitionManager.ensure_month_partition(...) является явным Python API. Он сериализует callers, создаёт partition, переносит подходящие default rows и атомарно attach-ит её. Bootstrap, Scheduler и CLI его автоматически не вызывают.

Retention также является только явным API:

PostgresMarketDataRetentionService.apply(
    policy=MarketDataRetentionPolicy(...),
    now=timezone_aware_utc_datetime,
)

Нет Retention env variables, background task или production CLI. enabled=False ничего не удаляет; None для типа означает отсутствие ограничения.

При явном запуске Retention:

  • полные зарегистрированные partitions с range_end <= cutoff удаляются;
  • оставшиеся/default rows старше точного cutoff удаляются отдельно;
  • выбранные типы обрабатываются одной транзакцией;
  • попытка удалить активную checkpoint Trade откатывает всю операцию.

Перед первым Retention run нужны backup, ручная проверка UTC cutoff, maintenance window и последующая проверка checkpoint/result counters.


10. Historical Access и Replay

Historical Access и Replay не входят в Bootstrap и не запускаются вместе с приложением.

Historical query:

  • synchronous;
  • использует отдельный repository call;
  • возвращает keyset page;
  • не удерживает общий snapshot между страницами.

Replay preparation:

  • synchronous и blocking;
  • выполняет READ ONLY REPEATABLE READ snapshot;
  • полностью материализует bounded plan;
  • закрывает PostgreSQL resources до playback.

Если preparation вызывается из async-приложения, внешний owner должен сам перенести blocking вызов с event loop. ReplaySession.run() является async one-shot operation и требует явную consumer factory. Default consumer и automatic startup отсутствуют.


11. Наблюдение за состоянием

Приложение пишет standard logs в stdout/stderr. Runtime logging намеренно не раскрывает полный transport payload. Отдельного HTTP readiness, metrics или alerting endpoint пока нет.

Полезные PostgreSQL проверки:

SELECT current_database(), current_user,
       current_setting('server_version');

SELECT version, name, applied_at
FROM public.storage_schema_migrations
ORDER BY version;

SELECT COUNT(*) AS trades
FROM market_data.trades;

SELECT venue, symbol, trade_id, executed_at, revision, updated_at
FROM market_data.trade_stream_checkpoints
ORDER BY venue, symbol;

SELECT data_type, partition_name, range_start, range_end
FROM market_data.partition_registry
ORDER BY data_type, range_start;

При подозрении на migration lock сначала исследуйте pg_stat_activity и pg_locks. Не завершайте backend, пока не установлен точный владелец и назначение transaction.


12. Troubleshooting

BOT_TOKEN or BOT_TOKEN_FILE is required

Telegram token обязателен для обычного Application startup независимо от feature flags. На host задайте BOT_TOKEN в защищённом локальном app/.env либо BOT_TOKEN_FILE; в Compose проверьте внешний BOT_TOKEN_SECRET_FILE и mount /run/secrets/bot_token. Не задавайте прямой token и file source одновременно.

PostgreSQL connection refused

Сначала определите место запуска:

  • host application → 127.0.0.1 или localhost и published port;
  • Compose bot → postgres:5432.

Проверьте pg_isready, database name, role и network.

Authentication не меняется после правки Compose

POSTGRES_* создаёт роль и базу только при инициализации пустого volume. Изменение YAML или env не обновляет существующую роль. Выполните контролируемое SQL/admin изменение либо restore в новый volume.

PostgreSQL остаётся unhealthy после обновления Compose

Проверьте role flags и owner базы. Hardened healthcheck намеренно отклоняет legacy volume, где application-role совпадает с bootstrap-admin или остаётся superuser. Предпочтительный путь — backup и restore в новый hardened volume; не отключайте healthcheck и не возвращайте bot доступ superuser ради быстрого запуска.

Storage flag отклонён

Storage требует включённый Trade Stream. Trade Stream требует явные TRADE_STREAM_WS_URL, TRADE_STREAM_SYMBOLS и EXCHANGE_BASE_URL.

Pool startup timeout

Проверьте DB health, DB_HOST, DNS, max_connections, pool min/max и network. Ошибка является fatal; in-memory fallback нет.

Migration history mismatch

Остановите Application, изучите backup и migration table. Не удаляйте и не переименовывайте записи вручную.

Migration 9 выглядит зависшей

Она может ждать ACCESS EXCLUSIVE lock либо выполнять полный backfill/index build. Остановите crash-looping bot, проверьте pg_stat_activity/pg_locks и не запускайте второй instance.

Startup ACK timeout

Проверьте точный WSS /connect, Origin из EXCHANGE_BASE_URL, symbols, credentials и TRADE_STREAM_SUBSCRIPTION_ACK_TIMEOUT_SECONDS.

Startup buffer overflow

Это обычно означает отсутствующий или слишком поздний ACK. Сначала диагностируйте protocol path; увеличение capacity не исправляет причину.

Heartbeat/reconnect failure

Runtime использует собственный Ping/Pong probe и одну reconnect attempt. Исправьте endpoint/network до контролируемого restart; автоматического backoff нет.

REST Recovery failure

Production Recovery использует /api/v1/aggTrades. Проверьте REST base URL, доступность endpoint и временное окно. Ошибка оставляет gate в terminal failure и завершает Runtime.

Corrupt/orphan checkpoint

Это ожидаемая integrity protection. Восстановите согласованный backup или диагностируйте durable rows. Не удаляйте checkpoint вручную.

Retention не удаляет старую Trade

Если Trade является активной checkpoint target, удаление запрещено, а Retention transaction откатывается. Это защита restart recovery.

Таблица Trades пуста

Проверьте оба feature flags. Сохраняются только принятые и идентичные дубликаты Canonical Trades; raw WebSocket/REST documents не хранятся.


13. Verification commands

Если не указано иначе, команды выполняются из каталога app.

13.1. Documentation and static typing

Из корня repository:

./scripts/check_python_types.sh
app/.venv/bin/python scripts/check_documentation_integrity.py

Допустимый результат — 0 errors, 0 warnings для Pyright и issues=0 для локального documentation gate. Gate не обращается к внешним URL и независимо от Git index проверяет *.md в корне и непосредственно в app, а также рекурсивно в app/src, app/tools и docs.

13.2. Default offline regression

.venv/bin/python -m pytest -q -p no:cacheprovider \
  -W error::ResourceWarning

pytest.ini исключает integration, stress и live по умолчанию. Acceptance-команда не создаёт .pytest_cache и считает ResourceWarning ошибкой.

13.3. Loopback integration

.venv/bin/python -m pytest -q -p no:cacheprovider \
  -W error::ResourceWarning \
  -m integration \
  tests/integration/market_data/acquisition/runtime

13.4. PostgreSQL integration

Harness разрушительно очищает market_data и собственную migration history. Он не создаёт PostgreSQL server, role или database. Ниже полный одноразовый локальный setup на проверенном PostgreSQL 16.14. Container публикуется только на 127.0.0.1, использует случайный test-only пароль и автоматически удаляется после проверки:

(
  set -eu
  container_name=dzentra-postgres-06030
  test_db_password="$(openssl rand -hex 24)"
  test_db_dsn="postgresql://dzentra_test:${test_db_password}@127.0.0.1:55430/dzentra_test_060_30"

  test -z "$(docker ps -a \
    --filter "name=^/${container_name}$" \
    --format '{{.Names}}')"

  docker run --rm --detach \
    --name "$container_name" \
    --publish 127.0.0.1:55430:5432 \
    --env POSTGRES_USER=dzentra_test \
    --env POSTGRES_PASSWORD="$test_db_password" \
    --env POSTGRES_DB=dzentra_test_060_30 \
    --health-cmd='pg_isready -U dzentra_test -d dzentra_test_060_30' \
    --health-interval=2s \
    --health-timeout=2s \
    --health-retries=30 \
    postgres:16.14-alpine@sha256:57c72fd2a128e416c7fcc499958864df5301e940bca0a56f58fddf30ffc07777

  trap 'docker stop -t 10 "$container_name" >/dev/null 2>&1 || true' EXIT

  test "$(docker port "$container_name" 5432/tcp)" = \
    '127.0.0.1:55430'

  attempt=0
  while test "$(docker inspect --format '{{.State.Health.Status}}' \
      "$container_name")" != healthy; do
    attempt=$((attempt + 1))
    test "$attempt" -lt 60
    sleep 1
  done

  DZENTRA_RUN_POSTGRES_TESTS=1 \
  DZENTRA_TEST_POSTGRES_DSN="$test_db_dsn" \
  .venv/bin/python -m pytest -q -p no:cacheprovider \
    -W error::ResourceWarning \
    -m integration \
    tests/integration
)

--rm вместе с cleanup trap удаляет disposable container и его anonymous data volume. Если container с таким именем или host port уже занят, процедура должна остановиться, а не переиспользовать неизвестную базу. Test password нельзя применять вне этого isolated harness.

В 060.30.7 отдельный compatibility harness проверил официальный PostgreSQL 17.10 image postgres@sha256:8189a1f6e40904781fc9e2612687877791d21679866db58b1de996b31fc312e4: полный integration-набор прошёл 90/90, шесть критических migration/checkpoint/snapshot/restart-сценариев — 10/10 прогонов, а контрольная durable-запись сохранилась после restart. Это evidence совместимости приложения, а не замена PostgreSQL 16.14 production deployment baseline.

Harness принимает только exact opt-in 1, loopback/local socket и безопасное имя базы. Проверка loopback DSN не ограничивает server bind, поэтому bind 127.0.0.1:55430:5432 проверяется отдельно. Никогда не передавайте production DSN.

13.5. Fixed stress

.venv/bin/python -m pytest -q -p no:cacheprovider \
  -W error::ResourceWarning \
  -m stress -k 'not soak' tests/stress

13.6. Standard и extended soak

DZENTRA_SOAK_SECONDS=120 \
.venv/bin/python -m pytest -q -p no:cacheprovider \
  -W error::ResourceWarning \
  -m stress -k soak \
  tests/stress/market_data/acquisition/runtime/test_trade_stream_runtime_stress.py

Для расширенной проверки замените 120 на 900.

13.7. Opt-in Dzengi live

Live harness использует только публичные Market Data operations и один явный symbol:

DZENTRA_RUN_LIVE_TESTS=1 \
DZENTRA_LIVE_REST_URL=https://api-adapter.dzengi.com \
DZENTRA_LIVE_WS_URL=wss://api-adapter.dzengi.com/connect \
DZENTRA_LIVE_SYMBOLS=BTC/USD_LEVERAGE \
DZENTRA_LIVE_TRADE_TIMEOUT_SECONDS=600 \
.venv/bin/python -m pytest -q -m live tests/live

Live verification требует доступ в интернет и не должна запускаться неявно в обычной regression.

13.8. Dependencies и compileall

Из корня repository:

app/.venv/bin/python -m pip check
(
  set -eu
  compile_cache_dir="$(mktemp -d)"
  trap 'rm -rf -- "$compile_cache_dir"' EXIT
  PYTHONPYCACHEPREFIX="$compile_cache_dir" \
    app/.venv/bin/python -m compileall -f -q \
    app/src app/tests scripts/check_documentation_integrity.py
)

Subshell ограничивает время жизни cleanup trap. compile_cache_dir остаётся отдельным временным каталогом и никогда не указывает на repository path.

13.9. Docker hardening

Static contract не требует Docker daemon:

.venv/bin/python -m pytest -q \
  tests/unit/core/test_config.py \
  tests/unit/infra/test_docker_hardening.py

Compose interpolation проверяется test-only readable paths без вывода развёрнутой конфигурации:

DZENTRA_COMPOSE_PROJECT_NAME=dzentra-hardening-check \
BOT_STOP_GRACE_PERIOD=30m \
POSTGRES_STOP_GRACE_PERIOD=60s \
BOT_TOKEN_SECRET_FILE=/dev/null \
POSTGRES_ADMIN_PASSWORD_SECRET_FILE=/dev/null \
DB_PASSWORD_SECRET_FILE=/dev/null \
docker compose -f ../infra/compose/docker-compose.yml config --quiet

Перед выпуском конкретного image повторяются build, non-root/read-only smoke и послойная проверка отсутствия project .env, .venv, tests, caches и специально созданного sentinel secret. Проверка только docker history недостаточна: нужно исследовать содержимое всех layers.


14. Граница данных и product limitations

  • Production Runtime сохраняет только Trades.
  • Quote/Candle repositories, Historical readers и Replay существуют, но production writers не подключены.
  • Initial historical backfill и completeness guarantee отсутствуют.
  • История начинается с первой успешно сохранённой записи либо ранее явно импортированной durable history.
  • Recovery ограничен доступностью и полнотой Dzengi REST.
  • Retention может сократить доступный период.
  • Historical pagination не является одним snapshot между страницами.
  • Replay воспроизводит deterministic storage order, а не исходный network packet order.
  • Replay полностью материализует bounded plan в памяти.
  • Нет automatic partition/retention scheduler.
  • Нет Replay API в Bootstrap, HTTP, Telegram или CLI.
  • Нет HA, distributed lease и leader election. Не запускайте несколько активных Runtime instances для одного venue + symbol.
  • Нет infinite reconnect/recovery retry.
  • Нет raw exchange-document storage.
  • Нет автоматических backup, metrics и alerting.
  • Online migration 9 не реализована.

15. Security checklist

  • не добавлять app/.env, tokens, API keys, passwords или production DSN в Git;
  • не печатать docker compose config без --quiet в записываемый log;
  • не расширять .dockerignore allowlist без повторной послойной проверки;
  • хранить Compose secret files и control file вне repository с mode 0444 внутри owner-only каталога mode 0700;
  • задавать уникальный стабильный DZENTRA_COMPOSE_PROJECT_NAME и не подключать PostgreSQL data volume от другой major version;
  • не использовать одну PostgreSQL role или один пароль одновременно для bootstrap-admin и Application;
  • считать ранее собранные без .dockerignore images скомпрометированными, не публиковать их и сменить попавшие в context credentials;
  • не публиковать PostgreSQL на внешние interfaces без необходимости;
  • не использовать placeholder change_me;
  • не направлять test harness на production database;
  • хранить backup отдельно от основного named volume;
  • перед migration 9 и первым Retention run проверять restore procedure;
  • не заменять контролируемую диагностику fatal startup автоматическим бесконечным restart loop;
  • не использовать down --volumes без отдельного разрешения на удаление точных database/runtime volumes.