# Trade Stream Production Runtime — эксплуатационное руководство **Статус:** Current; accepted in Build 060.30.2, CI-GATE.1 update **Область:** 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 \ --require-hashes \ -r app/requirements-dev.lock app/.venv/bin/python -m pip check ``` `app/requirements.txt` остаётся читаемым списком прямых production dependencies. Production image устанавливает полный транзитивный `app/requirements.lock` только с `pip --require-hashes`. `requirements-dev.txt` включает прямые production requirements и инструменты pytest/Pyright для developer environment, а установка выполняется из полного `requirements-dev.lock` с обязательными hashes. 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 uv pip compile app/requirements-dev.txt \ --constraints app/requirements.lock \ --generate-hashes \ --universal \ --python-version 3.12 \ --no-header \ --no-annotate \ --output-file app/requirements-dev.lock ``` После генерации каждого lock сохраняются только три русскоязычные служебные строки в начале файла; dependency records и hashes остаются машинными данными. Полный CI/canary contract приведён в [отдельном runbook](ci_gate.md). Нельзя вручную убирать 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 1–9 → запустить 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 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 необходимо: 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 1–9. ### 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.