Files
han-app/modules/module-07-bitrix-sync.md
T

480 lines
20 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.
# module-07. Проектная спецификация заглушки `bitrix-sync`
> Статус: целевая спецификация инфраструктурной заглушки MVP. CRM-синхронизация не реализуется.
> Источники: [`README.md`](README.md), [`arch-00-glossary.md`](arch-00-glossary.md), [`arch-01-system-architecture.md`](arch-01-system-architecture.md), [`arch-02-api-contracts.md`](arch-02-api-contracts.md), [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md), [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md), [`arch-05-agent-development-process.md`](arch-05-agent-development-process.md), [`module-01-api-backend.md`](module-01-api-backend.md), [`../../HAN_chat/deploy/init-managed-postgres.py`](../../HAN_chat/deploy/init-managed-postgres.py).
## 1. Назначение и жёсткая граница
На текущем этапе сервис доказывает только:
1. контейнер и FastAPI process стабильно запускаются в общем Compose;
2. сервис подключается к managed PostgreSQL по private network/TLS;
3. при старте и затем примерно раз в 60 секунд выполняется `SELECT 1`;
4. состояние доступно через health и защищённый status endpoint;
5. shutdown корректно останавливает loop и закрывает pool.
В этой версии **нет**:
- чтения/обработки `han_app.sync_queue`;
- CRM Contact map/create/update;
- вызовов Bitrix24 REST;
- CRM webhook `/bitrix/sync/webhook/contact`;
- доступа к OAuth `bitrix-local-app`;
- write-back в `han_app`, GUC `han.sync_suppress`;
- DLQ бизнес-задач и field mapping.
Упоминания полноценного sync в arch-01/02/03 описывают будущую целевую границу, а не функциональность этого stub. Расширение требует новой версии спецификации, migrations/GRANT, OpenAPI и contract tests.
## 2. Технологический профиль
- Python 3.12+, FastAPI, Pydantic v2, Uvicorn.
- SQLAlchemy 2 async/`asyncpg` либо прямой `asyncpg` pool; выбран SQLAlchemy async для единообразия с backend.
- Managed PostgreSQL; схема/role `bitrix_sync`.
- Один in-process periodic loop на replica.
- OpenTelemetry, JSON logging, pytest/anyio.
```text
bitrix-sync/
app/
main.py
settings.py
api/{health,status,auth,errors,schemas}.py
application/{db_probe,periodic_loop,state}.py
infrastructure/{database,observability}.py
tests/{unit,integration,contract}/
openapi.yaml
Dockerfile
docker-compose.yml
```
## 3. Runtime model
FastAPI lifespan:
```text
validate env
configure logs/OTEL
create bounded DB engine/pool
run initial probe with startup timeout
publish initial state
start exactly one periodic task
serve HTTP
on shutdown: signal stop → await/cancel sleep → finish bounded probe
→ close pool/OTEL → exit
```
HTTP process и loop разделяют thread-safe/async-safe immutable state snapshot. Router не выполняет probe для каждого status request.
## 4. Periodic loop
### 4.1. Период
Целевой интервал — примерно 60 секунд:
```text
BITRIX_SYNC_DB_CHECK_INTERVAL_SEC=60
```
Следующий запуск планируется от завершения предыдущего (`fixed-delay`), а не запускается параллельно. Добавляется jitter, например ±10%, чтобы несколько replicas не синхронизировались.
### 4.2. Initial check
Первый `SELECT 1` выполняется при startup до перехода в ready. Ошибка initial check не обязана завершать process: сервис остаётся live/not-ready и продолжает reconnect loop. Это позволяет восстановиться после временной недоступности managed PG без restart storm.
Невалидный env/DSN/TLS policy, напротив, является configuration error: process fail-fast.
### 4.3. Probe
Каждая проверка:
1. получает connection из pool с bounded acquire timeout;
2. выполняет параметризованный/constant `SELECT 1`;
3. проверяет результат `1`;
4. фиксирует monotonic duration и wall-clock UTC completion;
5. возвращает connection;
6. атомарно обновляет state.
Никаких table scans, DDL, schema writes и создания business rows.
### 4.4. Timeout
Общий probe timeout включает pool acquire + query:
```text
BITRIX_SYNC_DB_CHECK_TIMEOUT_SEC=5
```
На PostgreSQL задаются `connect_timeout`, `command_timeout`/`statement_timeout`. Timeout помечает check failed, отменяет query и гарантированно освобождает/инвалидирует connection.
### 4.5. Backoff
При успехе — обычный interval+jitter. При последовательных ошибках:
```text
delay = min(base * 2^(failures-1), max_backoff) + full_jitter
```
Ориентиры: base 5 с, max 60 с. Успех сбрасывает failure counter. Backoff не создаёт tight loop и не превышает readiness stale policy без явного статуса.
### 4.6. Prevention overlap
Одна task выполняет `await probe(); await sleep()`, поэтому overlap конструктивно невозможен. Дополнительно `asyncio.Lock`/single-flight защищает ручной internal trigger, если он когда-либо появится. В MVP trigger endpoint отсутствует.
При нескольких replicas каждая проверяет БД независимо; distributed lock не нужен, потому что `SELECT 1` безопасен и не является worker job.
## 5. Connection pool
Начальная конфигурация минимальна:
- pool size 12;
- max overflow 0;
- `pool_pre_ping=true` допустим, но не заменяет explicit probe;
- pool recycle меньше сетевого idle timeout провайдера;
- short acquire/connect/query timeout;
- TLS verify (`sslmode=verify-full` или эквивалент) с CA;
- `application_name=han-bitrix-sync`;
- search path только `bitrix_sync`.
Pool создаётся один раз и закрывается shutdown. Connection после network/protocol error invalidated. Пароль/DSN не логируются.
## 6. Enabled/disabled semantics
Сохраняется канонический `BITRIX_SYNC_ENABLED`.
### `true`
Для stub это означает: process запускает DB connectivity loop. Это **не** означает включённую CRM-синхронизацию. Status явно возвращает `mode=db_connectivity_stub`.
### `false`
- process и HTTP endpoint запускаются;
- DB pool можно не создавать, periodic loop не запускается;
- `/health/live``200`;
- `/health/ready``503` с `reason=sync_disabled`, как зафиксировано arch-04;
- internal status → `200`, `enabled=false`, `state=disabled`;
- CRM-функций всё равно нет.
Таким образом, disabled — явный no-op, а не скрытый success readiness.
## 7. HTTP API
### 7.1. `GET /health/live`
Без auth внутри Docker network. Не обращается к БД.
```json
{"status":"live"}
```
`200`, пока process/event loop обслуживает запросы.
### 7.2. `GET /health/ready`
Не выполняет новый DB query; читает snapshot.
`200`:
```json
{
"status": "ready",
"mode": "db_connectivity_stub",
"database": {
"status": "ok",
"last_success_at": "2026-07-10T09:00:00Z",
"age_seconds": 12
}
}
```
`503`:
```json
{
"status": "not_ready",
"reason": "database_unavailable",
"database": {
"status": "down",
"last_success_at": null,
"consecutive_failures": 3
}
}
```
Ready только если enabled, initial success был и последний success не старше:
```text
max(2 * interval + jitter budget, BITRIX_SYNC_READY_MAX_STALENESS_SEC)
```
Рекомендуемый default staleness 150 с. Error detail не содержит host/DSN.
### 7.3. `GET /internal/sync/v1/status`
Защита:
```text
Authorization: Bearer ${BITRIX_SYNC_SERVICE_TOKEN}
```
`X-Service-Token` можно поддержать только как migration compatibility; канонический вариант этого модуля — Bearer. Endpoint internal-only, edge не публикует.
```json
{
"service": "bitrix-sync",
"enabled": true,
"mode": "db_connectivity_stub",
"crm_sync_implemented": false,
"state": "healthy",
"started_at": "2026-07-10T08:00:00Z",
"last_check": {
"started_at": "2026-07-10T09:00:00Z",
"finished_at": "2026-07-10T09:00:00Z",
"success": true,
"duration_ms": 7,
"error_code": null
},
"last_success_at": "2026-07-10T09:00:00Z",
"consecutive_failures": 0,
"next_check_in_seconds": 48
}
```
Не возвращаются queue depth/dead letters, поскольку сервис их не читает. Поля, обещающие CRM run, не симулируются.
Invalid token → `401 service_unauthorized`, constant-time compare. Status endpoint не запускает probe.
## 8. State machine
```text
starting
├─ disabled → disabled
├─ initial success → healthy
└─ initial failure → degraded
healthy
├─ one/more failures → degraded
└─ shutdown → stopping
degraded
├─ success → healthy
└─ shutdown → stopping
```
Snapshot содержит start/check timestamps, last success/failure, consecutive failures, duration и safe error code: `db_connect_timeout`, `db_query_timeout`, `db_auth_failed`, `db_tls_failed`, `db_unavailable`, `unexpected_result`.
Auth/TLS/config ошибки могут быть classified non-transient и alertятся немедленно, но loop продолжает с max backoff, если env был syntactically valid.
## 9. PostgreSQL и права
Managed init уже создаёт:
- schema `bitrix_sync`;
- role `bitrix_sync_user`;
- search path `bitrix_sync`;
- отсутствие доступа к чужим схемам.
Для stub достаточно `CONNECT` к database и возможности `SELECT 1`; `USAGE` на `bitrix_sync` допустим для будущих migration/version checks. Таблицы не нужны. Alembic может иметь пустую baseline revision, чтобы зафиксировать ownership/version, но runtime не выполняет DDL.
К `han_app` **не выдаются GRANT** до реализации полноценной CRM sync. Это сознательно строже общего будущего требования. Когда появится sync:
- GRANT выдаётся точечно на `sync_queue`, mapping и необходимые columns;
- запрещён broad schema write;
- GUC/write-back и trigger contract проходят integration tests;
- обновляются deploy scripts и module spec.
`BITRIX_SYNC_APP_DATABASE_URL` из arch-04 в stub не требуется. Канонический runtime DSN stub — `BITRIX_SYNC_DATABASE_URL` с search path `bitrix_sync`.
## 10. Env
Уже канонические:
```text
APP_ENV=production-like
LOG_LEVEL=INFO
BITRIX_SYNC_ENABLED=true
BITRIX_SYNC_DATABASE_URL=postgresql://bitrix_sync_user:.../han_chat
BITRIX_SYNC_SERVICE_TOKEN=<secret>
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
```
Предлагаемые technical env, которые нужно синхронизировать с arch-04 до реализации:
```text
BITRIX_SYNC_DB_CHECK_INTERVAL_SEC=60
BITRIX_SYNC_DB_CHECK_TIMEOUT_SEC=5
BITRIX_SYNC_DB_CHECK_JITTER_RATIO=0.10
BITRIX_SYNC_DB_RETRY_BASE_SEC=5
BITRIX_SYNC_DB_RETRY_MAX_SEC=60
BITRIX_SYNC_READY_MAX_STALENESS_SEC=150
BITRIX_SYNC_DB_POOL_SIZE=2
BITRIX_SYNC_DB_POOL_RECYCLE_SEC=300
```
Старые `BITRIX_SYNC_CONTACT_*`, CRM URL/webhook/concurrency в stub не читаются и не должны создавать иллюзию sync. Их можно оставить в root env зарезервированными, но status явно сообщает `crm_sync_implemented=false`.
## 11. Observability
### Logs
JSON fields:
- timestamp, level, `service.name=bitrix-sync`;
- module, event, request_id, trace/span id;
- enabled/mode/state;
- check sequence, success, duration_ms, consecutive failures;
- error_code; shutdown reason.
Не логируются DSN, DB password, service token, SQL exception с credentials, host при принятой security policy. Сам `SELECT 1` можно не логировать каждый раз на INFO: success — DEBUG/metric, state transition — INFO, failure — WARN/ERROR с throttling.
### Metrics
- `bitrix_sync_db_probe_total{outcome}`;
- duration histogram;
- consecutive failures gauge;
- seconds since last success;
- state info/enabled;
- pool checked-out/wait duration/errors;
- HTTP requests/latency/status;
- loop lag;
- readiness.
Labels low-cardinality; DB host/error text не labels.
### Traces
Initial/periodic probe создаёт span `bitrix_sync.db_probe`; SQL statement sanitised/semantic convention. OTEL outage не влияет на readiness.
## 12. Security
- сервис только в `backend`/`observability` networks, без published port;
- `/internal/sync/v1/status` не edge-routed;
- Bearer token constant-time, secret только env/secret mount;
- managed PG private network + TLS verify;
- runtime DB role least privilege; никаких `han_app` grants;
- strict env validation; OpenAPI docs off production;
- non-root/read-only rootfs/tmpfs/drop capabilities;
- pinned dependencies/image, vulnerability scan;
- responses/logs не раскрывают DSN/credentials/internal stack.
## 13. Docker и healthcheck
Service:
- `expose: 8080`, без `ports`;
- networks `backend`,`observability`;
- env из root `.env`;
- managed PostgreSQL вне Compose;
- restart policy `unless-stopped`/platform policy;
- init/signal forwarding;
- graceful stop timeout больше probe timeout.
Container healthcheck использует `/health/live`, чтобы временная DB outage не создавала restart storm. Orchestrator/monitoring отдельно проверяет `/health/ready`.
Startup dependency не задаётся через fake PostgreSQL container. Application самостоятельно reconnect с backoff.
## 14. Graceful shutdown
На SIGTERM:
1. FastAPI перестаёт принимать новые запросы по server grace;
2. выставляется stop event;
3. interruptible sleep завершается немедленно;
4. новый probe не стартует;
5. текущий probe ждётся максимум shutdown budget, затем отменяется;
6. connection корректно возвращается/invalidate;
7. pool и telemetry flush закрываются;
8. task awaited — никаких `Task was destroyed`.
Shutdown не пишет бизнес-данные и не требует БД.
## 15. Ошибки и degraded behavior
| Ситуация | Process | Live | Ready | Loop |
|---|---|---|---|---|
| disabled | работает | 200 | 503 `sync_disabled` | не запущен |
| PG startup down | работает | 200 | 503 | retry/backoff |
| PG кратко down после success | работает | 200 | 503 после policy/stale | retry |
| wrong password | работает или fail-fast по policy | 200 если работает | 503 | max backoff + alert |
| malformed DSN/env | fail-fast | — | — | — |
| OTEL down | работает | 200 | по DB | продолжает |
| loop task unexpectedly died | работает кратко | 200 | 503 `worker_not_running` | supervisor/exit |
Необработанное исключение loop не должно молча оставить stale ready. Lifespan supervisor помечает not-ready и завершает process либо перезапускает task bounded; предпочтительно fail process после alert, чтобы orchestrator восстановил clean state.
## 16. Тестовая матрица
### Unit
- interval+jitter boundaries;
- exponential backoff/reset;
- state transitions/staleness;
- no-overlap single-flight;
- enabled/disabled;
- safe error classification/redaction;
- shutdown during sleep/probe.
### Integration
- initial and periodic `SELECT 1` на PostgreSQL;
- pool size/acquire timeout/recycle;
- DB unavailable then recovery without restart;
- query timeout/cancel and connection return;
- wrong credentials/TLS;
- no tables/writes and no `han_app` access;
- exact approximate 60-second scheduling with fake clock.
### Contract
- OpenAPI 3.1 parity;
- health/status schemas and HTTP codes;
- missing/wrong/correct `BITRIX_SYNC_SERVICE_TOKEN`;
- request-id/trace;
- internal endpoint absent through nginx.
### Runtime/failure
- SIGTERM at each loop phase;
- loop crash detection;
- long DB outage without log/reconnect storm;
- multiple replicas independently probe without overlap within replica;
- no secret/DSN in logs;
- Compose health does not restart solely on PG outage.
## 17. Definition of Done
- FastAPI process и один periodic loop реализованы;
- initial check и `SELECT 1` примерно каждые 60 с работают;
- timeout, jitter, backoff, overlap prevention и recovery проверены;
- pool bounded и graceful shutdown доказан;
- live/ready/status соответствуют contract и service token;
- enabled/disabled semantics явны;
- status всегда сообщает `mode=db_connectivity_stub`, `crm_sync_implemented=false`;
- runtime не читает `han_app`, queue или Bitrix CRM;
- least-privilege DB/network/container security соблюдены;
- structured logs/metrics/traces без secrets;
- OpenAPI, Docker healthcheck и tests готовы;
- future CRM boundary документирована и не реализована скрыто.
## 18. Решения, допущения и TBD
**Решения:**
- S1: stub выполняет только DB connectivity probe.
- S2: fixed-delay loop + jitter; overlap невозможен.
- S3: PG outage даёт live/not-ready, а не restart storm.
- S4: disabled даёт live 200, ready 503 `sync_disabled`.
- S5: `han_app` GRANT отсутствует до реальной sync.
- S6: status internal защищён Bearer `BITRIX_SYNC_SERVICE_TOKEN`.
**Допущения:**
- A1: одна replica MVP; несколько replicas безопасны, поскольку probe read-only.
- A2: interval 60 с и timeout 5 с достаточны для connectivity smoke.
- A3: managed PG CA/TLS параметры предоставляет ops.
**TBD:**
- S-TBD1: добавить proposed DB probe env в arch-04.
- S-TBD2: ready staleness threshold и alert thresholds после ops review.
- S-TBD3: fail-fast или persistent degraded при non-transient auth/TLS error.
- S-TBD4: baseline Alembic revision без таблиц — решение владельца deploy.
- S-TBD5: полноценная CRM sync, webhook, queue, grants, retries и mapping — отдельная будущая спецификация.