Runbook: Elevel / e.way ingestion deploy
Первичное подключение Elevel, проверка бесплатного full catalog, коммерческих данных, customer credentials и сцепления с существующими канониками.
Жёсткие правила
- Login/password не передавать в аргументах команд, логах, shell history,
.env, Git или документации. Секретный JSON создаёт secret manager во временном файле с mode0600; после seed файл удаляется и локально, и из контейнера. - Production config изменяется только через deploy-managed файлы. Ручное
изменение
/srv/tracium/infra/deploy/.envдопустимо лишь как аварийный мост и не считается завершённым rollout. fieldSet=maxзапрещён без отдельного ограниченного разрешения. Catalog и commerce jobs используют толькоfieldSet=standard.- Acceptance требует полный
SUPPLIER_ELEVEL_CATALOG_MAX_ITEMS=0и весь downstream-контур: raw storage → charnorm → matcher/LLM → canonical assignment.
Симптом
- Elevel отсутствует в
/suppliersadmin или в/cabinet/credentials; - системный tick не создаёт
supplier_offers/offer_observations; - customer credential сохраняется, но цена/остаток не меняются на клиентские;
- Elevel offers не сцепляются с существующими канониками;
- в provider trace появился
fieldSet=maxбез согласованного разрешения.
Pre-flight
- Убедиться, что deploy содержит Elevel в supplier catalog, connector wiring,
SUPPLIER_SYNC_SUPPLIERSиSUPPLIER_ELEVEL_*mapping. - Проверить deploy-managed значения:
SUPPLIER_ELEVEL_CATALOG_MAX_ITEMS=0, HTTPS base URL, разумные page size, RPS/burst иelevel:1h:trueв schedule. - Проверить, что master key Credentials BC доступен runtime через secret manager, не выводя его значение.
- Проверить global job-control gate и отсутствие активного Elevel tick перед первичным one-shot.
Seed системного credential
System metadata использует UUID
00000000-0000-0000-0000-000000000007. Secret-файл содержит только эту
запись; значение — JSON-строка формы {login,password}. Для точечного
onboarding CLI запускается с
--only-credential-id=00000000-0000-0000-0000-000000000007, поэтому секреты
других поставщиков не извлекаются и не ротируются.
Рекомендуемый порядок:
- Secret manager создаёт локальный временный файл mode
0600без вывода содержимого в stdout/stderr. - Выбирается maintenance-образ
worker-tools:<release-sha>с тем же SHA, что и production runtime. Обычный runtime/api-образ по контракту не содержитseed-credentials; при отсутствии tools-образа допустим только статический бинарник, собранный из того же release SHA и временно примонтированный в контейнер. Secret-файл монтируется read-only как/tmp/elevel-system-credential.json. - В maintenance-контейнере запускается
/usr/local/bin/seed-credentials --secrets-file=/tmp/elevel-system-credential.json --only-credential-id=00000000-0000-0000-0000-000000000007. CLI принимает runtimePOSTGRES_DSNи используетDATABASE_URLтолько как приоритетный явный override;CREDENTIALS_MASTER_KEY_V1уже должен быть передан через secret manager. - Оба временных файла удаляются; проверяется только metadata и наличие encrypted secret row:
SELECT c.id, c.supplier_ref, c.scope, c.status,
c.allowed_usages,
(s.credential_id IS NOT NULL) AS encrypted_secret_present
FROM supplier_credentials c
LEFT JOIN supplier_credential_secrets s ON s.credential_id = c.id
WHERE c.id = '00000000-0000-0000-0000-000000000007'
AND c.supplier_ref = 'elevel';Ожидается одна active system запись, encrypted_secret_present=true. Поля
ciphertext/nonce/key ID и тем более расшифрованный secret не выводить.
Catalog и commerce one-shot
Запустить два независимых Elevel job последовательно с production compose files. One-shot не должен останавливать постоянно работающий scheduler дольше проверки.
SUPPLIER_SYNC_SUPPLIERS='elevel:1h:true' \
SUPPLIER_SYNC_JOB_KIND=catalog \
SUPPLIER_SYNC_RUN_ON_START=false \
docker compose --env-file .env --env-file deploy/.env -p tracium \
-f deploy/docker/docker-compose.yml \
-f deploy/docker/docker-compose.web.yml \
-f deploy/docker/docker-compose.web.ci.yml \
-f deploy/docker/docker-compose.local-prod.yml \
run --rm --no-deps supplier-sync /usr/local/bin/supplier-sync --tick-once
SUPPLIER_SYNC_SUPPLIERS='elevel:1h:true' \
SUPPLIER_SYNC_JOB_KIND=commerce \
SUPPLIER_SYNC_RUN_ON_START=false \
docker compose --env-file .env --env-file deploy/.env -p tracium \
-f deploy/docker/docker-compose.yml \
-f deploy/docker/docker-compose.web.yml \
-f deploy/docker/docker-compose.web.ci.yml \
-f deploy/docker/docker-compose.local-prod.yml \
run --rm --no-deps supplier-sync /usr/local/bin/supplier-sync --tick-onceCatalog acceptance — terminal full-cycle event с full_cycle_exact=true.
Commerce cursor может закрываться несколькими ticks; продолжать до terminal
cycle, не вводя SKU cap.
Диагностика и DB assertions
WITH latest AS (
SELECT DISTINCT ON (so.id)
so.id, so.canonical_id, oo.prices, oo.stock_current, oo.raw_refs,
oo.credential_scope, oo.observed_at
FROM supplier_offers so
JOIN offer_observations oo ON oo.offer_id = so.id
WHERE so.supplier = 'elevel'
ORDER BY so.id, oo.observed_at DESC
)
SELECT count(*) AS offers,
count(*) FILTER (WHERE raw_refs ? 'goods') AS goods_payload,
count(*) FILTER (WHERE raw_refs ? 'price') AS price_payload,
count(*) FILTER (WHERE raw_refs ? 'remains') AS stock_payload,
count(*) FILTER (WHERE prices <> '{}'::jsonb) AS nonempty_price,
count(*) FILTER (WHERE stock_current <> '{}'::jsonb) AS nonempty_stock,
count(DISTINCT canonical_id) AS canonical_count,
max(observed_at) AS last_observed
FROM latest;
SELECT count(*) AS overlap_offers
FROM supplier_offers e
WHERE e.supplier = 'elevel'
AND EXISTS (
SELECT 1
FROM supplier_offers other
WHERE other.canonical_id = e.canonical_id
AND other.supplier <> 'elevel'
);Для overlap дополнительно вручную проверить несколько групп по normalized MPN, manufacturer/brand и name. Совпадение canonical ID без совместимой identity — ошибка, а не успех.
Provider/egress trace должен содержать nomenclature fieldSet=standard,
registrationNomenclatures, stockOfGoodsOffline, pricesOffline и ровно
ноль fieldSet=max.
Downstream и сцепление
После завершения ingestion выполнить порядок из
live-supplier-contour.md:
charnorm-worker --batch-onceдо объяснённого нулевого/стабильного tail;matcher-worker --batch-onceдо нулевого backlog;canonical-assignment-worker --batch-once, не прерывая активный LLM batch;- повторить DB assertions и сохранить overlap sample с positive и negative cases.
Бесплатный Elevel catalog может не содержать характеристики. Нулевой Elevel
offer_characteristic_raw сам по себе допустим: существующий canonical
обогащается данными других поставщиков после корректного identity match.
Customer credential и UI
- В ЛК открыть
/cabinet/credentials, убедиться, что в добавлении провайдера естьElevel / e.wayи схема login/password. - Создать тестовый customer credential через UI без раскрытия секрета в DevTools/logs. Проверить active health/status.
- Выполнить customer proposal для известного Elevel SKU и убедиться, что
observation имеет
credential_scope=customer, нужныйcustomer_ref, непустые current price/stock и не смешивается с другим клиентом. - В admin
/suppliersпроверить connector, system account, последние catalog и commerce ticks, request/error counters и отсутствие paid max calls.
Смягчение
- При auth rejection отключить только Elevel schedules, сохранить каталог и последнюю успешную коммерцию, затем безопасно ротировать DB-backed secret.
- При rate limit уменьшить deploy-managed RPS/burst и продолжить с durable cursor; не ставить постоянный SKU cap.
- При ошибочном matching остановить canonical assignment для затронутого набора и отправить disputes на moderation; ingestion цен/остатков можно продолжать отдельно.
- При любом незапланированном
fieldSet=maxнемедленно отключить Elevel jobs, сохранить trace/request IDs и проверить authorizer wiring до возобновления.
Контроль coverage characteristics
supplier_char_coverage — независимая служебная метрика характеристик, не
контур цен и остатков. Её пересчёт выполняет supplier-sync последовательно,
один supplier за тик. Если после простоя самая старая строка уже старше 48
часов, стартовый тик ограниченно догоняет ещё одну цель; это два
последовательных запроса с тем же 8-минутным лимитом, не параллельный batch.
После восстановления возвращается обычная ротация. Для крупных
Russvet/Systeme запрос может идти дольше дефолтных 5 минут при активной
загрузке. Целевое production-значение —
SUPPLIER_CHARACTERISTICS_RECONCILE_QUERY_TIMEOUT=8m; оно обязано
передаваться через deploy/docker/docker-compose.yml, а не правиться в
runtime .env вручную.
У worker-pool есть общий POSTGRES_STATEMENT_TIMEOUT=60s. Поэтому coverage
reconcile выставляет свой лимит transaction-local через set_config до
агрегации: иначе Systeme может быть отменён общим лимитом, а computed_at не
сдвинется, хотя тикер был запущен. Бюджет reconcile начинается после получения
соединения из worker-pool: очередь на свободное соединение не должна отнимать
время у самого SQL. Не повышать общий timeout ради этой фоновой метрики.
Перед выкладкой или повторным prod-health-check проверить, что нет client
queries дольше 30 минут. После выкладки убедиться, что computed_at Russvet
движется и остаётся моложе 48 часов. Если запрос не укладывается в 8 минут,
не повышать лимит дальше автоматически: сохранить план EXPLAIN, проверить
IO и оптимизировать сам запрос/его модель данных.
Текущая реализация агрегирует offer_characteristic_raw по supplier и offer
параллельным index-only проходом (до четырёх workers), с локально отключённым
sort, чтобы PostgreSQL мог объединить частичные группы. Настройки действуют
только внутри reconcile-транзакции и не меняют план live-ingestion. Возврат к
LATERAL-проверке каждой строки
supplier_offers запрещён: на крупных источниках она превращает проверку
здоровья в сотни тысяч случайных index probe и оставляет computed_at stale.
Миграция 0380_offer_characteristic_raw_supplier_offer_coverage_idx создаёт
для этого прохода offer_characteristic_raw_supplier_offer_coverage_idx
(supplier, offer_id) через CREATE INDEX CONCURRENTLY. После выкладки
убедиться, что индекс indisvalid=true и очередной reconcile обновил
computed_at stale-поставщиков; не заменять это повышением 8-минутного
тайм-аута.
backend-integration запускает Go-тесты уже после сборки Docker-образа.
Поэтому кэш модулей должен быть слоем образа, а не BuildKit cache mount:
последний недоступен runtime-контейнеру и маскирует зависание загрузки
модулей общим 30-минутным тайм-аутом job.
Устранение root cause
- Auth: исправить/ротировать secret через Credentials BC, не через
.env. - Transport: исправить HTTPS base URL, payload mapping или rate budget в deploy-managed конфигурации и выпустить обычным deploy.
- Matching: исправить identity mapping/нормализацию с sanitized fixture и regression test; не сливать каноники вручную без проверки negative case.
- Paid guard: оставить deny-by-default, добавить отдельный аудитируемый authorizer только для точных item IDs, credential, requester, reason и TTL.
Acceptance
- Elevel виден в admin и ЛК;
- system credential active и зашифрован в DB;
- full catalog завершён без cap, price/stock payload сохранены;
- customer credential возвращает customer-bound current price/stock;
- raw objects присутствуют, matcher/canonical assignment завершены;
- подтверждено хотя бы одно корректное multi-supplier canonical overlap и проверен negative case;
fieldSet=max=0, платные данные не запрашивались;- scheduler возвращён в штатный режим
elevel:1h:true.
Post-deploy health и coverage
После перезапуска supplier-sync тикер coverage стартует сразу и пересчитывает
самый протухший источник. prod-health-check может начаться раньше окончания
этого тяжёлого прохода; в CI он ждёт максимум 10 минут только обновления уже
протухшего снимка (CHAR_COVERAGE_BOOTSTRAP_WAIT_SECONDS=600). По окончании
окна проверка остаётся строгой: старый снимок — это ошибка, а не warning.