1125 lines
52 KiB
Markdown
1125 lines
52 KiB
Markdown
# 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.
|