← Harvester

Reliability

harvester · workzone

Что происходит при сбоях и больших объёмах: модуль не теряет данные и переживает падения. Любая синхронизация — это фоновая задача на SAQ поверх Redis, а не запрос в рамках HTTP-сессии. Администратор запускает синхронизацию и может закрыть вкладку: держать соединение не требуется, прерывание сессии не останавливает работу — прогон ведёт воркер. Дальше — путь устойчивости одного прогона: SyncRun checkpoint ретраи DLQ видимость. Сами режимы, которые гоняют эти прогоны, — на странице режимы синхронизации.

1 SyncRun — состояние прогона Одна запись на один прогон.
Сущность одного прогона
Каждый запуск синхронизации создаёт SyncRun — единую точку правды о ходе работы. В ней живут состояние (queued / running / succeeded / failed / cancelled), прогресс «обработано N из M», текущий чекпоинт, heartbeat воркера и накопленные ошибки. UI ничего не вычисляет сам: он читает SyncRun и рисует по ней прогресс и итог.
progress N / M
checkpoint cursor
heartbeat alive at
errors K в DLQ
2 Чекпоинт и resume Большой объём синхронизируется по частям.
Постранично и с продолжением
Источник на сотни тысяч сущностей выкачивается постранично, и после каждой порции прогон фиксирует позицию — checkpoint. Прерванный прогон возобновляется (resume) с последнего чекпоинта, а не с нуля: уже обработанное не переделывается. Страницу, не дописанную до чекпоинта в момент краха, resume проходит заново — без дублей, потому что запись в хранилище идемпотентна (Load). Саму механику курсоров и постраничной выборки даёт sources; здесь — только то, как прогон опирается на неё, чтобы пережить объём.
Heartbeat: падение воркера и перезапуск
Пока воркер активен, он обновляет heartbeat в SyncRun. Остановившийся heartbeat означает, что воркер упал (перезапуск пода, пересоздание контейнера): прогон подхватывается заново и продолжается с последнего чекпоинта. Интервал heartbeat — 30 с, а падением молчание считается после трёх пропущенных ударов (90 с): этого хватает пережить паузу GC или краткий рестарт пода, но зависший прогон подхватывается за минуту-две. Оба значения — фиксированные константы воркера, не настройка источника. Падение воркера превращается в паузу, а не в потерю. Heartbeat бьётся из отдельной корутины, независимой от стека ретраев: иначе долгая пауза внутри попытки выглядела бы как смерть воркера и вызывала ложный перезапуск. Короткий backoff (потолок 60 с) безопасен — он ниже порога молчания 90 с; длинные паузы вообще не держат воркер, а уходят в отложенную постановку.
Resume — с бюджетом свежести, не бесконечно
Resume устраняет последствия краха, а не заменяет полноценный прогон: продолжать с чекпоинта осмысленно, пока разрыв короткий. Если перерыв превысил бюджет свежести чекпоинта (6 часов — фиксированная константа модуля, не настройка источника), курсор и каталог источника успевают устареть, а объекты — исчезнуть: чекпоинт отбрасывается, и прогон перезапускается с нуля. Авто-resume применяется только к краху (остановившийся heartbeat): прогон, отменённый администратором (cancelled), терминален, его чекпоинт не возобновляется — следующий прогон стартует с нуля. Так суточная пауза источника не приводит к загрузке устаревшей выборки. Resume применим только к инкрементальному прогону коннектора, чей поток глобально упорядочен по времени изменения (флаг манифеста): у неупорядоченного потока watermark не гарантирует, что всё более раннее обработано. Reconciliation и точечный DLQ-retry не возобновляются и курсор не двигают — их выборка не отражает фронтир потока.
crash · в бюджете прогон продолжается с последнего чекпоинта
page 1 page 2 ✗ crash ↻ resume page 3
crash · бюджет истёк чекпоинт отброшен, прогон с нуля
page 1 page 2 ✗ crash ⏳ бюджет истёк ↺ с нуля page 1
cancelled · админ прогон терминален, следующий стартует с нуля
page 1 page 2 cancelled новый прогон page 1
3 Ретраи с backoff Временный сбой источника.
Повтор с нарастающей паузой
Сетевой обрыв, таймаут, 429 или 5xx от API источника — повод повторить, а не упасть. Запрос повторяется с backoff: пауза между попытками растёт, чтобы не перегружать источник и переждать всплеск. После исчерпания попыток элемент не теряется, а уходит дальше по пути, в DLQ.
Что ретраить, что сразу в DLQ — по классу ошибки
Решение «повторить или отложить» зависит от класса ошибки, а не от конкретного кода. Транзиентные (пройдут сами: всплеск нагрузки, кратковременная недоступность) — ретраятся. Перманентные (повтор бессмыслен: запрос негоден, нет прав, нет объекта, данные не нормализуются) — без попыток в DLQ. Это не привязано к HTTP: классификация — слой над протоколом, поэтому источники иной природы (S3, БД, файловые шары, почта — без HTTP-кодов) ложатся в тот же механизм без переделки ядра. Коннектор объявляет свой классификатор ошибок, ядро применяет единый retry/DLQ-цикл.
транзиентные → ретрай 429 · 500 · 502 · 503 · 504 · 408 · сетевые таймауты и обрывы
перманентные → сразу DLQ 400 · 401 · 404 · 422 · ошибки нормализации и валидации
HTTP-маппинг — лишь дефолт. Таблица кодов выше — штатное соответствие «код → класс» для HTTP-источников, которое коннектор вправе переопределить: код, означающий разное у разных API, классифицируется по поведению конкретного источника, а не по букве протокола. Пример — 403 у GitHub/GitLab: чаще это rate-limit, а не отказ в доступе, поэтому перед отправкой в DLQ коннектор сверяется с Retry-After / X-RateLimit-Remaining и трактует такой 403 как транзиентный.
Параметры backoff
Попыток на HTTP-страницу — 5 (первая + 4 повтора), in-memory в рамках задачи-воркера. Пауза — экспоненциальная с full jitter: база 1 с, множитель ×2, потолок 60 с. Потолок выбран намеренно ниже порога молчания heartbeat (90 с), чтобы короткий ретрай никогда не выглядел как смерть воркера. Все четыре — фиксированные константы воркера, не настройка источника.
attempts 5 (1 + 4)
base 1 с
factor ×2
cap 60 с
jitter full
Два уровня ретраев: память против очереди
Короткие задержки (≤ потолка 60 с) — это in-memory ретрай страницы прямо в задаче: он лечит транзиентный сбой, не теряя прогресс прогона. Долгие паузы (минуты-часы: большой Retry-After, деградация источника) воркер не отсыпает — он откладывает задачу обратно в очередь (SAQ defer / re-enqueue) и освобождает слот. Так бюджет свежести чекпоинта (6 часов) реализуется отложенной постановкой, а не удержанием воркера на sleep. Если источник прислал Retry-After, он переопределяет расчётный backoff.
4 Dead Letter Queue Один сбойный элемент.
Partial failure, а не all-or-nothing
Item, который не удалось обработать даже после ретраев, откладывается в DLQ вместе с причиной — и прогон идёт дальше по остальным. Это стратегия частичного отказа: тысяча сущностей загрузилась, три сбойных лежат в очереди разбора. Прогон при этом терминируется как succeeded с error_count > 0 — завершён успешно, но не безупречно: счётчик ошибок отделяет частичный сбой от полного провала (failed). DLQ — durable-таблица dead_letters в Postgres, не эфемерное состояние прогона: backoff-ретраи выше живут в памяти задачи-воркера и считаются секундами, а сбойный item ждёт ручного разбора и переживает перезапуск воркера.
обработанные хранилище
сбойные DLQ
5 Видимость сбоев Сбой виден, понятен и исправим.
Статус прогона
SyncRun показывает «обработано N/M, K в DLQ» — по ней видно и ход, и итог любого прогона.
Уведомление о провале
Провалившийся прогон поднимает уведомление — админ узнаёт о сбое сам, не открывая панель. Канал доставки (внутри платформы, письмом) выбирает система уведомлений, не Harvester.
Повторный запуск
«Повторить сбойные» поднимает incremental-прогон вручную со scope из очереди (dlq retry) — после починки источника или прав их не нужно собирать заново. Обработанные уходят из DLQ, снова упавшие ждут следующего разбора; поля и жизненный цикл держит модель данных.
Где это смотрят
Экран мониторинга синхронизаций и разбор сбойных items живут в Admin Panel — Harvester сюда только поставляет данные SyncRun.