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

Индексатор Ethereum: начальная загрузка и непрерывный поток

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

Начальная загрузка истории должна когда-то закончиться, а новые блоки продолжают появляться. В этом уроке соединим backfill фиксированного диапазона с непрерывным чтением от checkpoint. Результат — между двумя этапами нет скрытого промежутка, а поток уведомлений остаётся ускорителем, а не единственным источником полноты.

Предполагаются boundedranges, идемпотентный журнал и восстановление поколения. Индексатор и RPC не запускались. Мы выбираем простую серверную модель последовательного опроса, чтобы показать гарантию непрерывности. WebSocket и специализированная очередь могут появиться позже без изменения смыслового договора.

Фиксированная цель начальной загрузки

При старте читаем выбранную верхнюю границу H по политике сети. Затем обрабатываем от блока развёртывания или checkpoint доH включительно. Новые блоки вышеH не расширяют начальную цель постоянно. Процесс сможет объявить backfill завершённым и перейти к следующему этапу.

После завершения снова читаем выбранную границу и обрабатываем checkpoint+1 до нового уровня. Именно checkpoint задаёт начало, а не момент подписки на события. Если за время backfill появились блокиH+1…H+10, они попадут в следующий диапазон. Нет необходимости рассчитывать, успело ли WebSocket-уведомление прийти в нужную миллисекунду.

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

Один шаг синхронизации

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

export async function syncStep({ client, store, deployment, readRange, saveRange }) {
  const checkpoint = await store.currentCheckpoint(deployment);
  await store.assertCurrentBranch(client, checkpoint);
  const target = await client.getBlockNumber();
  const start = BigInt(checkpoint.blockNumber) + 1n;
  if (start > target) return { status: 'caught_up', target };
  for (const range of blockRanges(start, target, 50n)) {
    const logs = await readRange(client, deployment, range);
    const block = await client.getBlock({ blockNumber: range.toBlock });
    await saveRange({ deployment, range, block, logs });
  }
  return { status: 'range_finished', target };
}

store.assertCurrentBranch выполняет описанную сверку checkpoint, а saveRange атомарно сохраняет события и курсор. Показанная сигнатура readRange является адаптером над функцией предыдущего урока с уже замкнутым ABI. Это зависимость контроллера, не новая неизвестная RPC-команда. Названия методов соответствуют собственному интерфейсу сервиса.

Верхняя граница здесь текущий head для учебной модели. Для публичной сети функция выбора target заменяется явно выбранной политикой. Нельзя принять этот фрагмент за универсальную production-настройку финальности. Все диапазоны имеют известный target и поколение обработки.

Уведомления ускоряют следующий шаг

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

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

WebSocket-соединение имеет жизненный цикл и ошибки. После восстановления не предполагаем, что сервер автоматически прислал все пропуски. Выполняется backfill от сохранённой точки. Схема надёжной обработки остаётся одной, поэтому не требуется отдельный непроверенный алгоритм «восстановления подписки».

Ошибка одной порции

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

Повторы ограничиваются по категории. Временный тайм-аут допускает задержку и повтор, несовместимый ABI требует исправления конфигурации, найденная смена ветви запускает reorg. Один общий бесконечный retry скрывает причину и может удерживать сервис в неработающем состоянии без полезной диагностики.

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

Начальная загрузка API

Пока backfill не завершён до выбранного target, API может показывать частичную историю с явной нижней и верхней границей. Оно не сообщает «вся история», если ещё не обработаны ранние блоки. Либо персональный интерфейс ждёт готового публичного источника, либо показывает понятное состояние подготовки по продуктовой политике.

Проекция курса17 может выглядеть корректной раньше завершения полного реестра, но это не гарантирует полноту остальных номеров. Общий статус сервиса относится к его области загрузки. Для независимых экземпляров можно иметь самостоятельные checkpoints и статусы, не блокируя весь каталог из-за одного источника.

Для упражнения задайте H=200, появление блоков201–205 во время backfill и отказ порции150–199. Объясните, почему процесс не перескакивает к205 до исправления пробела. Затем потеряйте уведомление о новом блоке: периодический шаг всё равно найдёт недостающее. Ожидаемый итог — поток и начальная история соединены через устойчивый курсор. RPC описан в Ethereum JSON-RPC.

Граница двух режимов

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

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

Если процесс живёт долго, очередь сигналов можно сворачивать до одного признака «нужно проверить head». История блоков всё равно восстанавливается из источника по непрерывным диапазонам. Это экономит память без потери договорённого покрытия.

Когда начальная загрузка не завершена, API не изображает частичную историю полной. Пользователь видит cutoff, начальную границу и состояние building. Уже построенные карточки можно показывать по продуктовой политике, но поиск «событие не найдено» означает лишь отсутствие в обработанной части. После завершения состояние меняется атомарно вместе с источником и поколением.

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