Files
wiki/tasks/orchestrator/DEV_TASK_ORCH5_WEBHOOK_DEDUP.md
2026-06-03 09:20:01 +03:00

8.1 KiB
Raw Blame History

DEV TASK — ORCH-5: идемпотентность webhook (M-7)

Проект: orchestrator | Сервер: slin@82.22.50.71 | Репо: /home/slin/repos/orchestrator | Контейнер: orchestrator (8500) Ветка: feature/ORCH-5-webhook-dedup из свежего main (main содержит ORCH-1/1b/2/4/6/7).

⚠️ ГРАБЛЯ push (ORCH-7, НЕ повтори): после коммитов git log origin/main..HEAD (твои коммиты есть), после push git log origin/main..origin/feature/ORCH-5-webhook-dedup ДОЛЖЕН показать твои коммиты. НЕ отчитывайся «PR готов» пока remote-ветка реально не содержит коммитов.

Проблема (M-7)

Webhook-хендлеры (src/webhooks/gitea.py, src/webhooks/plane.py) при каждом запросе делают INSERT INTO events и затем диспетчеризуют → enqueue_job(...). Dedup'а НЕТ — delivery-id нигде не читается (проверено grep'ом: нигде). Повторная доставка одного и того же вебхука (ретрай Gitea/Plane, сетевой реброд, ручной replay) → повторный enqueue → дубль запуска конвейера. Это тот же класс бага, что инцидент ET-009 (параллельные конвейеры на одном репо).

Цель: сделать обработку webhook идемпотентной — один delivery обрабатывается ровно один раз. Повторный с тем же delivery-id → залогировать как duplicate и вернуть {"status":"duplicate"} БЕЗ диспетчеризации/enqueue.

Текущее состояние (верифицировано по живому коду)

  • gitea_webhook (gitea.py:42): проверяет HMAC X-Gitea-Signature → INSERT events → диспетч (push/pr/status).
  • plane_webhook (plane.py:52): HMAC X-Plane-Signature → INSERT events → ORCH-6 project-filter → диспетч.
  • Таблица events (db.py:14): id, timestamp, source, event_type, payload, processedнет delivery-id, нет UNIQUE.
  • Gitea шлёт заголовок X-Gitea-Delivery (GUID на доставку). Plane свой delivery-заголовок шлёт НЕнадёжно.

Что сделать

  1. Миграция БД (паттерн _ensure_column, db.py:74):
    • Добавить колонку events.delivery_id TEXT (idempotent через _ensure_column).
    • Создать UNIQUE-индекс: CREATE UNIQUE INDEX IF NOT EXISTS idx_events_delivery ON events(delivery_id) WHERE delivery_id IS NOT NULL. (partial unique — старые строки с NULL delivery_id не конфликтуют).
  2. Вычисление delivery-id (helper, напр. в src/webhooks/_dedup.py или в db.py):
    • Gitea: request.headers.get("X-Gitea-Delivery"). Если пусто → fallback sha256(source + event_type + body).
    • Plane: попробовать X-Plane-Delivery/X-Hook-Delivery если есть; иначе fallback sha256("plane" + body) (стабильный hash тела — повторная доставка идентичного тела даст тот же id). Префиксуй source чтобы gitea/plane не пересекались: итоговый delivery_id = f"{source}:{raw_or_hash}".
  3. Идемпотентный INSERT + ранний выход:
    • Заменить текущий INSERT INTO events (source,event_type,payload) на INSERT с delivery_id и INSERT OR IGNORE (или ловить sqlite3.IntegrityError).
    • Если строка НЕ вставилась (delivery уже есть) → logger.info("webhook duplicate delivery_id=..."), вернуть {"status":"duplicate"} и НЕ вызывать диспетч/enqueue.
    • Если вставилась (новый delivery) → продолжить как сейчас (диспетч).
    • ⚠️ Порядок: dedup-проверка ПОСЛЕ HMAC-верификации (невалидную подпись по-прежнему 401), для plane — реши: dedup до или после ORCH-6 project-filter. Рекомендация: dedup СРАЗУ после HMAC и INSERT, ДО project-filter (чтобы повторная доставка не делала лишнюю работу). Но НЕ сломай ORCH-6 поведение {"status":"ignored"} для unknown project на ПЕРВОЙ доставке.
  4. Не трогать саму диспетч-логику, enqueue, ORCH-6 фильтр, stage_engine, очередь.

Ограничения

  • 🚫 НЕ трогай: nginx, openclaw.json, .env-секреты, deploy-хук, Plane-webhook is_active, ORCH-1/1b/2/4/6/7, stage_engine.py, очередь, HMAC-проверку (только добавляешь dedup ПОСЛЕ неё).
  • ⚠️ Миграция должна быть restart-safe и не падать на уже существующей БД с данными (как _ensure_column для jobs).
  • ⚠️ 9 pre-existing webhook-тестов (401) — это baseline, НЕ чинить и НЕ ломать дополнительно.
  • Conventional Commits: feat(webhook): dedup deliveries by delivery_id (M-7), feat(db): add events.delivery_id + unique index, test(webhook): ....

Тесты (в контейнере)

  • Новый tests/test_webhook_dedup.py:
    • первый webhook с delivery-id X → обрабатывается (status accepted), enqueue вызван.
    • повторный webhook с тем же delivery-id X → {"status":"duplicate"}, enqueue НЕ вызван (мок enqueue_job, assert call_count не вырос).
    • два РАЗНЫХ delivery-id → оба обработаны.
    • gitea без X-Gitea-Delivery → fallback hash работает (повтор того же тела = duplicate).
    • plane fallback hash аналогично.
    • миграция на «старой» БД без колонки delivery_id не падает.
  • Прогон: IMG=$(docker inspect orchestrator --format '{{.Config.Image}}'); docker run --rm -v /home/slin/repos/orchestrator:/code -w /code --entrypoint python3 $IMG -m pytest tests/ -q
  • Baseline: сейчас 136 passed, 9 pre-existing 401 — не трогать. Новые зелёные, старые не сломать.

Acceptance (проверит Стрим вживую)

# Что Критерий
1 миграция events.delivery_id + partial UNIQUE index, restart-safe, не падает на проде-БД
2 gitea dedup повтор X-Gitea-Delivery → duplicate, без enqueue
3 plane dedup повтор тела → duplicate (fallback hash)
4 HMAC цел невалидная подпись по-прежнему 401
5 ORCH-6 цел unknown project на первой доставке → ignored (не сломано)
6 тесты новые зелёные, baseline 136 не сломан
7 прод пересобран из ветки, health ok, /queue ok, миграция применилась на живой БД

Деплой + отчёт

  • docker compose up -d --build && sleep 6 && curl -s :8500/health && curl -s :8500/queue
  • Проверь что миграция применилась: docker exec orchestrator python3 -c "import sqlite3;print([r[1] for r in sqlite3.connect('/app/data/orchestrator.db').execute('PRAGMA table_info(events)')])" — должна быть delivery_id.
  • После каждого блока — короткий отчёт + результат.
  • Запушь ветку (ПРОВЕРЬ remote!), открой PR в main. НЕ мержи — мерж делает Стрим после проверки.
  • Других исполнителей на репо нет — только ты, не паникуй про параллельные сессии.