Перейти к содержанию

Индексатор Ethereum: восстановление индексатора и наблюдаемость

Оглавление курса «Web3: вход, смарт-аккаунты и индексатор» · Урок 29 из 30.

Мы научились сохранять события, проверять ветвь блоков, строить проекцию и ограничивать нагрузку. Теперь рассмотрим обычную эксплуатационную ситуацию: индексатор остановился, диск или процесс был недоступен, а после запуска нужно продолжить работу без потери истории и без ложной свежести. Результатом урока станет явная процедура восстановления и небольшой безопасный паспорт состояния.

Все листинги являются учебными договорами. Индексатор, база, сеть и аварийные сценарии здесь не запускались. Процедура описывает порядок решений, а не подтверждает готовность производственного сервиса. Значения пределов выбираются для локальной модели; реальные окна хранения, число повторов и допустимое отставание определяются эксплуатационным профилем.

Что переживает перезапуск

Источник продолжения — долговечный checkpoint, сохранённый атомарно с событиями и проекцией. Номер последнего блока в памяти процесса недостаточен: приложение могло получить ответ, но не успеть завершить транзакцию базы. После перезапуска чтение такого номера способно перескочить через отсутствующие записи.

Паспорт checkpoint включает environmentId, chainId, адрес реестра, номер блока, blockHash и версию политики. Эти поля отвечают на вопрос «какое именно состояние мы построили». Не подставляем новый локальный контракт в старый паспорт только потому, что адрес выглядит знакомым. После сброса среды собственный environmentId предотвращает случайное продолжение другой истории.

Состояние выполняющейся операции читателя хранится отдельно. Перезапуск индексатора не отменяет подписанные операции, cookie-сессии и резерв оплаты автоматически. У каждой системы есть свой источник истины и процедура восстановления. Общий технический сбой не разрешает синхронно пометить все процессы как неуспешные.

План возобновления

Создадим функцию, которая получает уже прочитанные факты и возвращает следующий шаг. Она не выполняет восстановление сама. Такой план можно показать оператору и записать в журнал до начала массового изменения состояния.

export function recoveryPlan({ configured, stored, canonicalHash, leaseOwned }) {
  if (!leaseOwned) return { action: 'wait', reason: 'another-worker' };
  if (!stored) return { action: 'backfill', from: configured.deploymentBlock };
  if (stored.environmentId !== configured.environmentId ||
      String(stored.chainId) !== String(configured.chainId) ||
      stored.catalogAddress !== configured.catalogAddress) {
    return { action: 'stop', reason: 'environment-mismatch' };
  }
  if (canonicalHash === undefined) return { action: 'retry-read', reason: 'rpc-unknown' };
  if (canonicalHash !== stored.blockHash) return { action: 'find-common-anchor' };
  return { action: 'resume', from: BigInt(stored.blockNumber) + 1n };
}

Вход leaseOwned означает проверенную возможность работать текущему процессу. В реальном хранилище одного флага недостаточно: нужна защита от старого работника, который продолжает писать после потери аренды. Поколение или fencing token проверяется при каждой фиксации порции. Иначе два процесса могут по очереди перезаписывать checkpoint противоположными представлениями.

Неопределённый canonicalHash отличается от другого hash. Если RPC недоступен, ищем повторное наблюдение, а не объявляем реорганизацию. Только действительный ответ в согласованном окружении позволяет сравнить ветвь. Это правило предотвращает разрушительное восстановление из-за обычного тайм-аута.

Наблюдаемость без лишних секретов

Полезный статус содержит обработанный блок, наблюдаемую вершину, поколение проекции, время последней успешной фиксации и безопасный код ошибки. Пользователь видит свежесть данных, а оператор получает контекст остановки. Содержимое подписанных сообщений, session cookie и секреты RPC для этого не нужны.

{
  "state": "recovering",
  "environmentId": "LOCAL_ENVIRONMENT_ID",
  "indexedBlock": "205",
  "observedHead": "210",
  "projectionGeneration": "generation-3",
  "lastCommittedRange": { "from": "201", "to": "205" },
  "errorCode": "RPC_TEMPORARILY_UNAVAILABLE"
}

LOCAL_ENVIRONMENT_ID в листинге — явный заполнитель, который должен быть заменён из конфигурации. Значения показывают ожидаемую форму ответа, а не реальное наблюдение сети. Персональную информацию читателя в этот публичный объект не добавляем. Если API показывает часы, время относится к серверу и сопровождается понятным смыслом, например «последняя успешная фиксация», а не неопределённым updatedAt.

Метрика отставания в блоках удобна, но не является универсальным переводом в секунды. Скорость цепочки может меняться. Рост расстояния показывает проблему относительно выбранной вершины, однако не доказывает её причину. Дополняем его длительностью обработки порции, числом повторов и размером очереди, не превращая один показатель в автоматический диагноз.

Восстановление из копии

Копия базы должна включать события, checkpoint, версии проекции и связь активного поколения. Если скопировать только таблицу courses, потеряется возможность объяснить её происхождение и построить историю. Если скопировать только события, понадобится повторное построение, что допустимо при известной версии алгоритма и полноте данных.

Перед переключением публичного API новое поколение проходит договор согласованности: известен обработанный срез, события упорядочены, ветвь проверена, проекция относится к тому же checkpoint. В этой статье мы описываем необходимые проверки, но не запускаем их. До их выполнения API может выдавать старый подтверждённый срез с явным состоянием recovering либо временно закрыть определённую функцию по политике продукта.

Резервная копия не доказывает, что её ветвь всё ещё актуальна. После восстановления снова сравниваем anchor с сетью. При несовпадении ищем общую сохранённую точку и выполняем известный replay. Если точки нет, останавливаем автоматическое продолжение и строим историю из поддерживаемого исходного диапазона, сохраняя объяснение ограничения.

Ожидаемые аварийные сценарии

Процесс остановился после получения логов, но до commit. После запуска durable checkpoint остался прежним, и диапазон читается снова. Идемпотентное сохранение не создаёт двойных канонических записей, а проекция строится в том же договоре. Процесс остановился после commit: восстановление начинается со следующего блока, потому что паспорт уже зафиксирован.

Старый работник вернулся после потери аренды. Его запись должна быть отклонена хранилищем из-за устаревшего поколения. Новый работник сохраняет единую последовательность. Одного логирования предупреждения недостаточно: защита требуется в самой операции фиксации, где конкурирующие изменения реально встречаются.

Локальная сеть была создана заново. Конфигурация содержит другой environmentId, поэтому recoveryPlan возвращает stop. Оператор выбирает новый индекс или явный сброс лабораторных данных. Прежняя история не получает новую подпись происхождения автоматически.

После урока можно объяснить, откуда продолжается индексатор и что означает его статус. Последняя глава соберёт кабинет читателя, сетевые действия и проекции в один проектный договор передачи, сохраняя независимость этих процессов.

Сигнал тревоги и действие оператора

Событие «checkpoint не менялся несколько циклов» полезно для наблюдаемости, но само по себе не разрешает очищать базу. Цепочка могла не дать новых блоков, фильтр мог не найти событий, а источник мог временно ограничить чтение. Оператор получает контекст: выбранную вершину, последний диапазон и безопасную категорию ошибки.

Процедура восстановления определяет отдельные решения: повторить чтение, сменить согласованный источник, искать общий anchor или начать новое построение. Каждое решение оставляет запись причины. Это позволяет следующему разработчику понять, почему система сменила поколение, вместо поиска случайного автоматического сброса в журнале процесса.

Оглавление серии