51 KiB
Trade Stream Production Runtime — эксплуатационное руководство
Статус: Current; accepted in Build 060.30.2
Область: Trades Feed, persistent Market Data Storage и связанные проверки
Версия документа: 1.0
Связанные документы
- Текущая архитектура Trades Feed
- Архитектура Build 060.30
- Пример переменных окружения
- Docker Compose
- Storage migrations
1. Назначение
Документ описывает настройку, запуск, остановку и проверку Production Trades Feed. Он не заменяет процедуру deployment конкретной среды и не содержит production credentials.
Главные правила:
- PostgreSQL нужен обычному запуску приложения даже при выключенном Market Data Storage: Bootstrap всегда создаёт базовые таблицы журнала и balance snapshots.
- Trade Stream и persistent Storage выключены по умолчанию.
- Storage разрешён только вместе с Trade Stream.
- Persistent checkpoint включается вместе со Storage; отдельного флага нет.
- Ошибка включённого Trade Runtime, Storage или Recovery фатальна.
- Retention, monthly partition creation и Replay не запускаются автоматически.
- 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 допустимы два безопасных пути:
- использовать текущий проверенный hardened PostgreSQL 16.14 image, закреплённый одновременно tag и digest;
- переходить на 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 1–9
→ запустить 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 не превращается в бесконечный
restartloop; - обязательный 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 1–9 и 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 необходимо:
- запретить push и deployment такого image;
- сменить
BOT_TOKEN, database password, API keys и другие credentials, которые находились в build context; - удалить соответствующие local images/build cache и registry artifacts по принятой инфраструктурной процедуре;
- проверить registry и deployment audit logs;
- после 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 1–9.
8.1. Migration 9
Migration 9 является блокирующей. Она:
- берёт общий partition advisory lock;
- получает
ACCESS EXCLUSIVElocks на Trades, Quotes и Candle revisions; - назначает существующим строкам общий
replay_sequence; - добавляет defaults,
NOT NULL, checks, triggers и indexes.
Длительность, WAL, temporary files и требуемое место растут вместе со всем уже накопленным Market Data объёмом.
Для нетривиальной базы обязательно:
- остановить все writers и bot instances;
- создать и проверить backup;
- измерить migration на копии сопоставимого объёма и той же PostgreSQL major version;
- проверить свободное место и допустимое maintenance window;
- запустить ровно один Application instance;
- наблюдать locks и logs до commit;
- проверить 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 READsnapshot; - полностью материализует 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; - не расширять
.dockerignoreallowlist без повторной послойной проверки; - хранить Compose secret files и control file вне repository с
mode
0444внутри owner-only каталога mode0700; - задавать уникальный стабильный
DZENTRA_COMPOSE_PROJECT_NAMEи не подключать PostgreSQL data volume от другой major version; - не использовать одну PostgreSQL role или один пароль одновременно для bootstrap-admin и Application;
- считать ранее собранные без
.dockerignoreimages скомпрометированными, не публиковать их и сменить попавшие в 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.