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/Dockerfilego build ./... clean (см. § Smoke)
141 миграция БДbackend/migrations/0001..0141Чистая БД проходит full chain (verified 2026-05-15 a4f49dfc)
Compose-сервис stock-detail-warmerdeploy/docker/docker-compose.ymlHealthcheck :9093/readyz
Admin UI cursor-pagination + warm-filterweb/admin/src/features/ingestion/components/IngestionLogsTab.tsx + CanonicalListScreen.tsxpnpm tsc --noEmit clean

Pre-flight (за 30 минут до старта)

  1. Secrets (через Vault / Kubernetes Secret / shell wrapper):

    • POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB — БД-инстанс под Tracium.
    • POSTGRES_DSNpostgres://...?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_V132-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.
  2. Включить миграции на первый деплой:

    • POSTGRES_MIGRATE_ON_START=true на api-server.
    • После успешного 141 → выставить false и redeploy api-server (миграции одноразово).
  3. Включить DB-driven scheduler:

    • INGESTION_SCHEDULER_DB_DRIVEN=true на supplier-sync.
  4. Сервисы supplier API (внешние URL):

    • SUPPLIER_ETM_BASE_URL=https://ipro.etm.ru/api/v1
    • SUPPLIER_IEK_BASE_URL=https://lk.iek.ru
    • SUPPLIER_SYSTEME_BASE_URL=https://api.systeme.ru
    • SUPPLIER_RUSSVET_BASE_URL=https://cdis.russvet.ru/rs
    • SUPPLIER_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_state priority 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-limitFull sweep 421k SKU = ~2.3h prices phaseSingle tick раз в 30 мин — допустимо
ETM 1 login / 2 min rate-limitПараллельные ticks с одним credential дерутся за sessionSession 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 рабочего endpointSUPPLIER_IEK_BASE_URL=https://lk.iek.ru, Basic credentials из DB, SUPPLIER_IEK_REMAINS_ENABLED=true только после проверки
Russvet 3-фазный pipelineFull sweep ~3h (catalog + stock + prices)SUPPLIER_RUSSVET_STOCK_PAGES_PER_TICK ограничивает один tick
Customer credentials (ETM/IEK/…) могут expire у поставщика403 auth_rejected → fetch_error tickAdmin UI должен флагать expired при ротации

Откат

В случае проблем:

  1. Остановить 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');
  2. Остановить контейнеры:

    docker compose -p tracium stop supplier-sync stock-detail-warmer
  3. 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.