ETM ingestion: fresh-base prod deploy runbook
Дата: 2026-05-15. Применяется для первого выкладывания связки ETM Tier-1 + Tier-2 на пустую prod-БД.
Что выкладывается
| Артефакт | Источник | Verify |
|---|---|---|
Backend Go-бинари (api-server, supplier-sync, stock-detail-warmer, …) | deploy/docker/resources/go-prod/Dockerfile | go build ./... clean (см. § Smoke) |
| 141 миграция БД | backend/migrations/0001..0141 | Чистая БД проходит full chain (verified 2026-05-15 a4f49dfc) |
Compose-сервис stock-detail-warmer | deploy/docker/docker-compose.yml | Healthcheck :9093/readyz |
| Admin UI cursor-pagination + warm-filter | web/admin/src/features/ingestion/components/IngestionLogsTab.tsx + CanonicalListScreen.tsx | pnpm tsc --noEmit clean |
Pre-flight (за 30 минут до старта)
-
Secrets (через Vault / Kubernetes Secret / shell wrapper):
POSTGRES_USER,POSTGRES_PASSWORD,POSTGRES_DB— БД-инстанс под Tracium.POSTGRES_DSN—postgres://...?sslmode=requireдля prod.MINIO_ROOT_USER,MINIO_ROOT_PASSWORD— S3-совместимое хранилище (либо AWS keys черезS3_ACCESS_KEY/S3_SECRET_KEY).S3_BUCKET_RAW— bucket для RAW-выгрузок поставщиков.S3_ENDPOINT— URL S3.CREDENTIALS_MASTER_KEY_V1— 32-byte (base64-encoded) master key для AES-GCM шифрования supplier_credentials secrets. Сгенерировать новый, НЕ из dev:openssl rand -base64 32.REDIS_ADDR— Redis для session cache + idempotency.KAFKA_BROKERS— Kafka cluster (для outbox dispatcher).OTEL_*(опц.) — OpenTelemetry endpoint.ADMIN_API_KEY— токен админ-API.LLM_BASE_URL,LLM_API_KEY— gateway для charnorm / matcher / category-classifier.
-
Включить миграции на первый деплой:
POSTGRES_MIGRATE_ON_START=trueна api-server.- После успешного 141 → выставить
falseи redeploy api-server (миграции одноразово).
-
Включить DB-driven scheduler:
INGESTION_SCHEDULER_DB_DRIVEN=trueна supplier-sync.
-
Сервисы supplier API (внешние URL):
SUPPLIER_ETM_BASE_URL=https://ipro.etm.ru/api/v1SUPPLIER_IEK_BASE_URL=https://lk.iek.ruSUPPLIER_SYSTEME_BASE_URL=https://api.systeme.ruSUPPLIER_RUSSVET_BASE_URL=https://cdis.russvet.ru/rsSUPPLIER_DKC_BASE_URL=...(зависит от prod-договора).
Шаги деплоя
1. Apply migrations (one-shot)
POSTGRES_MIGRATE_ON_START=true \
docker compose -p tracium up -d api
docker logs -f tracium-api-1 | grep "migrate\|migrations applied"
# Ожидание: "goose: successfully migrated database to version: 141"После завершения миграций — выставить POSTGRES_MIGRATE_ON_START=false и перезапустить api для production-mode (без auto-migrate).
2. Seed system credentials через CLI
# Шифруем секреты master-ключом, кладём в supplier_credential_secrets.
docker exec tracium-api-1 /usr/local/bin/seed-credentials \
--secrets-file=/path/to/seeds.jsonФормат seeds.json:
{
"<etm-system-credential-uuid>": "{\"login\":\"...\",\"password\":\"...\"}",
"<iek-system-credential-uuid>": "{\"login\":\"...\",\"password\":\"...\"}",
...
}UUID-ы и метаданные credential’ов создаются через admin UI заранее (/Кокпит → Коннекторы → Подключить ETM).
3. Включить supplier_credentials.warehouse_scope для ETM
ETM warehouse_discovery auto-probe может вернуть 0 stores при IP-throttle. Чтобы не зависеть от throttle:
UPDATE supplier_credentials
SET warehouse_scope = ARRAY['regional_center:14', 'regional_center:35', ...]
WHERE supplier_ref = 'etm' AND scope = 'system';Список доступных кодов на credential — спросить у ETM-менеджера. Если scope не сидить — warmer auto-probe попробует 11 кодов (СПБ/Урал/Самара/Москва/ЮГ/Сибирь/Казань/Чехов/Малоярославец/Воронеж/Владивосток) и persistит found set.
4. Снять global-stop guard (job-control)
Migration 0124 НЕ сидит row ingestion_job_controls scope='global' target='*'. Если строка появилась после ручного клика “Stop All” в admin UI — снять:
UPDATE ingestion_job_controls
SET enabled = true, reason='production start', updated_by='ops', updated_at=now()
WHERE scope = 'global' AND target = '*';Если строки нет — guard по умолчанию AllowDispatch=true. Проверь:
SELECT scope, target, enabled FROM ingestion_job_controls WHERE scope='global';5. Запустить supplier-sync и stock-detail-warmer
docker compose -p tracium up -d supplier-sync stock-detail-warmerПосле запуска:
supplier-syncсидит DB-driven scheduler, начинает poll’итьingestion_schedulesкаждую секунду.stock-detail-warmerзапускаетscheduler.TickerкаждыеSTOCK_DETAIL_WARMER_TICK_INTERVAL(default 5m), читаетsku_warm_statepriority queue.
6. Первый ETM tick
ETM commerce-system schedule создаётся через admin UI или сидится в ingestion_schedules SQL. Минимальный seed:
INSERT INTO ingestion_schedules
(job_key, supplier_ref, job_kind, credential_scope, target_policy,
interval_seconds, enabled, priority, stale_threshold_seconds)
VALUES
('supplier-sync:etm:commerce-system', 'etm', 'commerce', 'system', 'full',
1800, true, 100, 14400);stale_threshold_seconds=14400 (4 часа) — ETM full sweep 421k SKU @ 1 req/sec = ~2.3 часа prices phase.
7. Verify deploy
После 5-10 минут после старта supplier-sync:
-- Tier-1 ETM observations должны появиться:
SELECT count(*) as obs_count,
count(*) FILTER (WHERE stock_current != '{}'::jsonb) as with_stock,
count(*) FILTER (WHERE prices != '{}'::jsonb) as with_prices
FROM offer_observations
WHERE credential_ref = '<etm-system-cred-uuid>'
AND observed_at > now() - INTERVAL '15 min';Через ~30-60 минут (после первого warm_state_update phase):
SELECT count(*) FROM sku_warm_state WHERE supplier_ref = 'etm';После очередного STOCK_DETAIL_WARMER_TICK_INTERVAL:
SELECT count(*) FROM offer_detail_observations
WHERE observed_at > now() - INTERVAL '15 min';8. Готово
ETM Tier-1 (bulk) грузит каталог раз в interval_seconds. Tier-2 (warmer) обогащает low-stock SKU per-SKU /goods/{id}/remains.
Известные pre-existing ограничения
| Ограничение | Влияние | Mitigation |
|---|---|---|
ETM 1 req/sec data rate-limit | Full sweep 421k SKU = ~2.3h prices phase | Single tick раз в 30 мин — допустимо |
ETM 1 login / 2 min rate-limit | Параллельные ticks с одним credential дерутся за session | Session cache в Redis (110m TTL) переиспользует |
IEK stock optional: публичный API не документирует остатки, текущий DB credential получает authenticated 404 на /api/commerce/Remains | Держать SUPPLIER_IEK_REMAINS_ENABLED=false; IEK commerce пишет prices/goods без stock и не должен быть partial. Включать stock только после live-probe рабочего endpoint | SUPPLIER_IEK_BASE_URL=https://lk.iek.ru, Basic credentials из DB, SUPPLIER_IEK_REMAINS_ENABLED=true только после проверки |
| Russvet 3-фазный pipeline | Full sweep ~3h (catalog + stock + prices) | SUPPLIER_RUSSVET_STOCK_PAGES_PER_TICK ограничивает один tick |
| Customer credentials (ETM/IEK/…) могут expire у поставщика | 403 auth_rejected → fetch_error tick | Admin UI должен флагать expired при ротации |
Откат
В случае проблем:
-
Остановить ingestion:
UPDATE ingestion_job_controls SET enabled = false, reason='emergency stop', updated_by='ops' WHERE scope = 'global' AND target = '*';Если строки нет — INSERT:
INSERT INTO ingestion_job_controls (scope, target, enabled, reason, updated_by) VALUES ('global', '*', false, 'emergency stop', 'ops'); -
Остановить контейнеры:
docker compose -p tracium stop supplier-sync stock-detail-warmer -
Rollback миграции — НЕТ. Миграции с
+goose Downпомечены как “Irreversible” для 0124, partition migrations не предназначены для отката (потеряются данные partitions). Восстановление — из PG backup.
Связанные документы
- ADR-0059 — ETM batch shape (per-store remains pair-iter, per-50 prices).
- ADR-0058 — Unified retention BC.
- ADR-0043 — Ingestion orchestration.
docs/superpowers/specs/2026-05-15-etm-stock-warm-strategy-design.md— Tier-2 design.docs/superpowers/plans/2026-05-15-etm-stock-detail-warm-strategy.md— Tier-2 implementation plan.