supplier-sync runbook

Worker-процесс ingestion: стучится в поставщика через 12h-тикер, нормализует ответ, апсертит offer’ы, пишет raw-payload в S3. Public HTTP отсутствует — все пробы loopback-only на 127.0.0.1:9091.

Start

Локальная разработка (с mock-серверами трёх поставщиков):

# Поднять инфраструктуру + mock-ETM + mock-IEK + mock-Systeme
docker compose --profile test up -d
 
# Одноразовый тик — быстрая проверка без запуска тикера
docker compose --profile test run --rm supplier-sync /usr/local/bin/supplier-sync --tick-once
 
# Убедиться, что данные записаны
docker compose exec postgres psql -U tracium -d tracium \
  -c "SELECT supplier, count(*) FROM supplier_offers GROUP BY supplier;"

Ожидаемый результат после первого тика с дефолтными seed-файлами:

 supplier | count
----------+-------
 etm      |    30
 iek      |    29

Это результат по умолчанию, потому что SUPPLIER_SYNC_ENABLED остаётся etm,iek. systeme подключён в compose и коде, но включается только явным opt-in.

Чтобы прогнать systeme локально:

SUPPLIER_SYNC_ENABLED=etm,iek,systeme \
docker compose --profile test run --rm supplier-sync /usr/local/bin/supplier-sync --tick-once

На дефолтном mock-корпусе systeme даёт один happy-path offer (C9F34106); остальные seed-SKU намеренно проверяют 200 data:null, 429, 403 и generic unknown-SKU поведение.

Без --profile test mock-серверы не стартуют → supplier-sync стучится по несуществующим адресам и тик падает с ошибкой подключения. Это нормально: процесс всё равно поднимается для /healthz smoke.

Manual live first-layer smoke

Полный live contour runbook с downstream проверками: ../../40-operations/runbooks/live-supplier-contour.md.

Для ручной проверки реальных поставщиков используется тот же entrypoint:

TRACIUM_E2E_LIVE_SUPPLIERS=1 \
TRACIUM_E2E_LIVE_ENABLED=etm,iek,systeme \
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 \
sh ./scripts/backend_e2e_compose.sh

Live mode:

  • включается только явным TRACIUM_E2E_LIVE_SUPPLIERS=1;
  • не поднимает mock-etm, mock-iek, mock-systeme;
  • использует active catalog_sync credentials из БД, а не локальные SUPPLIER_*_LOGIN/PASSWORD/API_TOKEN;
  • для последующих charnorm-worker, matcher-worker Tier 3 и canonical-assignment-worker resolver использует внешний CLI Proxy LLM_BASE_URL=https://lgm.tracium.ru/v1; LLM_API_KEY хранится в ignored deploy/.env;
  • валидирует только first layer: supplier_offers, latest offer_observations, raw_refs и наличие raw objects в MinIO;
  • не запускает matcher/proposal pipeline и не проверяет customer-scoped сценарии.

Обязательные env в live mode:

  • SUPPLIER_ETM_BASE_URL
  • SUPPLIER_IEK_BASE_URL
  • SUPPLIER_SYSTEME_BASE_URL

Скрипт fail-fast завершится до docker compose up, если обязательный base URL отсутствует. Секреты поставщиков должны быть заранее заведены в supplier_credentials / supplier_credential_secrets.

Multi-supplier scheduling (Phase 1-b)

Тикер обходит поставщиков из env SUPPLIER_SYNC_ENABLED (CSV, порядок сохраняется) последовательно — по одному RunOnce на supplier за тик. Падение одного поставщика не блокирует остальных: тикер логирует warning и продолжает с следующего имени; последняя ошибка возвращается наружу для учёта в метриках.

EnvDefaultPurpose
SUPPLIER_SYNC_ENABLEDetm,iekCSV-список активных supplier-имён

Интервал (SUPPLIER_SYNC_TICK_INTERVAL) и прогон-при-старте (SUPPLIER_SYNC_RUN_ON_START) описаны ниже в разделе Env — они применяются к тикеру целиком, не per-supplier.

Параметры запуска --tick-once работают по той же схеме: проходят по всем Enabled последовательно и завершают процесс.

Поддерживаемые supplier-имена

  • etm — см. connectors/etm.md.
  • iek — см. connectors/iek.md (iter-1-b).
  • systeme — см. connectors/systeme/README.md (iter-1-c).
  • russvet — см. connectors/russvet.md.

Расширение списка — через fx-группу ingestion.bundles: каждый infra-модуль публикует ingdom.ConnectorBundle с своим Name; ingestion-aggregator собирает map, тикер использует ключи как идентификаторы в SUPPLIER_SYNC_ENABLED.

Troubleshooting

  • no bundle registered → supplier из CSV не имеет infra-модуля или у него пустой BaseURL. Проверить deploy/.env на SUPPLIER_<NAME>_BASE_URL.
  • no active credentialsStaticCredentialProvider в ingestion/di.go не содержит записи для этого supplier. Добавить запись в map и редеплоить.
  • Упал ровно один supplier, остальные ОК → нормальное поведение iter-1-b: тикер продолжает, smoke-check через curl http://127.0.0.1:9091/readyz покажет last_tick_fresh.

Новые env для IEK (Phase 1-b)

EnvDefaultPurpose
SUPPLIER_IEK_BASE_URLIEK API endpoint (mock-iek в dev, https://lk.iek.ru или полный https://lk.iek.ru/api/products в prod)
SUPPLIER_IEK_LOGINОбязателен при непустом BaseURL
SUPPLIER_IEK_PASSWORDОбязателен при непустом BaseURL
SUPPLIER_IEK_SEED_FILEbackend/config/iek-seed.yamlПуть к seed-yaml

Новые env для Systeme (Phase 1-c)

EnvDefaultPurpose
SUPPLIER_SYSTEME_BASE_URLSysteme API endpoint (http://mock-systeme:9200 в compose test profile)
SUPPLIER_SYSTEME_API_TOKENОбязателен при непустом BaseURL
SUPPLIER_SYSTEME_SEED_FILEbackend/config/systeme-seed.yamlПуть к seed-yaml
SUPPLIER_SYSTEME_PRICE_METHODgetpriceOverride price endpoint на getekaterinburgprice

Health endpoints

Loopback-only, доступ через docker compose exec:

docker compose exec supplier-sync wget -qO- http://127.0.0.1:9091/healthz
docker compose exec supplier-sync wget -qO- http://127.0.0.1:9091/readyz
docker compose exec supplier-sync wget -qO- http://127.0.0.1:9091/metrics

/readyz семантика:

  • Агрегирует все health.Checker (PG/Redis/Kafka/MinIO) — первый fail даёт 503.
  • Плюс last_tick freshness: если последний успешный тик был больше 24h назад, возвращается 503 с сообщением last_tick: stale: .... На свежем старте, когда тика ещё не было, возвращается 200 с last_tick: none (решение iter-1: no-tick-yet — это не деградация).

Env

ПеременнаяДефолтНазначение
SUPPLIER_ETM_BASE_URLhttp://mock-etm:9000URL поставщика. Для ручной отладки можно переключить на https://itest2.etm.ru/api/v1 или https://ipro.etm.ru/api/v1. Пустое значение отключает тикер, процесс всё равно поднимается для healthcheck-smoke.
SUPPLIER_ETM_LOGIN / SUPPLIER_ETM_PASSWORDmock-login / mock-passwordETM credentials. Реальные — для ручного прогона против itest2 или prod.
SUPPLIER_SYNC_TICK_INTERVAL12hПериод тика. Dev может сократить до 5m для быстрой итерации.
SUPPLIER_SYNC_RUN_ON_STARTfalsetrue — один тик при старте (dev feedback). В prod оставлять false чтобы рестарты не хаммерили поставщика.
POSTGRES_DSNиз composeПодключение к основной БД.
KAFKA_BROKERSkafka:9092Брокеры для outbox dispatcher.
S3_*из composeraw-payload bucket.

Graceful shutdown

docker compose stop supplier-sync триггерит fx OnStop:

  1. Loopback-HTTP закрывается первым (5s grace).
  2. Тикер получает cancel на run-ctx; текущий RunOnce должен респектнуть ctx и освободить advisory-lock (lease rollback).
  3. Если тик не завершается за ShutdownTimeout (30s), Stop() возвращает timeout error — fx логирует и форсит exit.

Lease release автоматический через tx rollback на ctx cancel, так что даже принудительный kill не блокирует следующий запуск.

Прицельное наблюдение

  • Ticker log pattern: scheduler: tick failed (уровень warn) на временные ошибки, scheduler: tick panicked (уровень error) на panic внутри RunOnce.
  • Orchestrator TickReport: одна структурированная лог-строка на RunOnce (см. internal/core/ingestion/app/orchestrator.go).

Известные ловушки при добавлении нового supplier

При подключении нового infra-модуля в supplier-sync обязательно проверить:

1. ratebuckets.Module() нужен в platformModules()

ingetm.Module() (и любой supplier-модуль с rate-limiting) требует ratebuckets.RatePolicy из platform/ratebuckets. Модуль не транзитивно включается через ingestion.Module() — его надо явно добавлять в platformModules() в cmd/supplier-sync/main.go.

Симптом отсутствия: fx wiring fail при старте:

missing type: ratebuckets.RatePolicy

2. Миграции должны быть в Docker-образе

providePostgresPool запускает миграции из cfg.Postgres.MigrationsDir (env POSTGRES_MIGRATIONS_DIR, default /app/migrations). В Dockerfile обязателен:

COPY backend/migrations /app/migrations
ENV POSTGRES_MIGRATIONS_DIR=/app/migrations

Симптом: migrations directory does not exist при старте.

3. CredentialRef должен быть UUID-совместимой строкой

offer_observations.credential_ref — тип uuid в Postgres. staticCredentialProvider в internal/core/ingestion/di.go задаёт CredentialRef как строку. В iter-1 используются nil-namespace UUID:

  • ETM: 00000000-0000-0000-0000-000000000001
  • IEK: 00000000-0000-0000-0000-000000000002

Каждый новый supplier должен получить уникальный nil-namespace UUID (инкрементировать последний октет). Phase 2 заменит это на реальный Credentials BC.

Симптом: invalid input syntax for type uuid: "etm-system" в логах upsert.

4. Провайдеры DI должны возвращать конкретные типы, не интерфейсы

При наличии нескольких supplier-модулей в одном fx-графе каждый должен возвращать конкретный тип (*etm.Connector, *iek.Connector), а не ingdom.Connector. Иначе fx не может различить провайдеров по типу.

Симптом: missing types: domain.Connector (did you mean *etm.Connector?).

5. systeme по умолчанию не включён в SUPPLIER_SYNC_ENABLED

Даже если mock-systeme поднят и env SUPPLIER_SYSTEME_* заданы, worker не пойдёт в него без явного systeme в CSV.

Симптом: mock работает, но в логах/БД нет ни одного systeme-тика.

6. 403 у Systeme — это не session expiry

Для systeme 403 маппится в AuthRejected, а не в refresh session. Если видите серию 403, проверяйте token и статус его блокировки у партнёра, а не retry/login логику.

ADR-0033: imitation service mandated

Каждый поставщик имеет mock в deploy/docker/resources/mock-<name>/ который отдаёт канонический fixture-набор под compose profile test. Не направляйте local dev на реальные supplier sandbox без явного TRACIUM_<SUPPLIER>_REAL=1 opt-in. См. ADR-0033.