Files
dzentra_bot/docs/operations/trades_feed_runtime.md

1111 lines
51 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Trade Stream Production Runtime — эксплуатационное руководство
**Статус:** Current; accepted in Build 060.30.2
**Область:** Trades Feed, persistent Market Data Storage и связанные проверки
**Версия документа:** 1.0
---
## Связанные документы
- [Текущая архитектура Trades Feed](../architecture/trades_feed.md)
- [Архитектура Build 060.30](../migrations/build_060_30_architecture.md)
- [Пример переменных окружения](../../app/.env.example)
- [Docker Compose](../../infra/compose/docker-compose.yml)
- [Storage migrations](../../app/src/storage/migrations.py)
---
## 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:
```bash
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:
```bash
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:
```text
DB_HOST=127.0.0.1
DB_PORT=5432
```
Для bot container внутри одной Compose network:
```text
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
```text
TRADE_STREAM_ENABLED=false
MARKET_DATA_STORAGE_ENABLED=false
```
Trade Runtime и отдельный Market Data pool не создаются. PostgreSQL всё
равно должен быть доступен для legacy `init_schema()` и журнала.
### 5.2. Volatile Trade Runtime
```text
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
```text
TRADE_STREAM_ENABLED=true
MARKET_DATA_STORAGE_ENABLED=true
```
Приложение открывает отдельный pool, применяет migrations, сохраняет
Trades и persistent checkpoint, выполняет Hydration и Startup Recovery.
### 5.4. Host startup
Из каталога `app`:
```bash
.venv/bin/python -m src.main
```
Ошибка включённого Trade Runtime или его неожиданное нормальное
завершение останавливает всё приложение, а не оставляет Telegram polling
в частично работающем режиме.
---
## 6. Startup и graceful shutdown
### 6.1. Application startup
```text
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
```text
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
```text
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
```text
отменить 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:
```text
/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:
```bash
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 не используется.
Проверка конфигурации не должна печатать развёрнутую модель:
```bash
(
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:
```bash
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:
```bash
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
```
После запуска порядок проверки такой:
```text
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 нельзя исправлять вручную.
Проверка состояния:
```sql
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`
защищает новый файл, а любой неуспешный шаг завершает процедуру.
```bash
(
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:
```text
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 проверки:
```sql
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:
```bash
./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
```bash
.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
```bash
.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 пароль
и автоматически удаляется после проверки:
```bash
(
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
```bash
.venv/bin/python -m pytest -q -p no:cacheprovider \
-W error::ResourceWarning \
-m stress -k 'not soak' tests/stress
```
### 13.6. Standard и extended soak
```bash
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:
```bash
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:
```bash
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:
```bash
.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 без вывода
развёрнутой конфигурации:
```bash
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.