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

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

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

Индексатор не должен читать всю историю одним огромным запросом. RPC-сервис ограничивает диапазоны, размер ответа и число обращений. В этом уроке разделим известный промежуток на небольшие включённые диапазоны и сохраним точный смысл границ. Результат — ни один блок не пропущен и не требуется неограниченное повторение при отказе endpoint.

Предполагаются нормализованные события и блок развёртывания. Используем серверный Public Client viem2.37.9 выбранной сети. Учебная программа и запросы не запускались. Размер порции ниже является демонстрационной политикой, а не универсальным пределом любого провайдера.

Включённые границы

Запрос событий обычно задаёт fromBlock и toBlock включительно. Если порция начинается с100 и заканчивается149, следующая начинается с150. При переходе на149 возникнет перекрытие, которое потребует дедупликации; при переходе на151 потеряется блок150. Даже если в пропущенном блоке нет событий, ошибка алгоритма остаётся и проявится позже.

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

Номера блоков представлены bigint. Преобразование большого номера через Number не нужно для расчёта диапазона. Для хранения используем объявленную схему, но арифметика границ остаётся точной. Размер порции тоже является положительным целым значением; нулевая порция могла бы создать бесконечный цикл.

Генератор диапазонов

Сначала определим чистую функцию границ без RPC. Она возвращает последовательность допустимых пар и поэтому ясно показывает порядок будущих запросов.

export function* blockRanges(start, end, size = 50n) {
  if (start < 0n || end < start || size <= 0n) {
    throw new Error('Некорректные границы');
  }
  for (let fromBlock = start; fromBlock <= end; fromBlock += size) {
    const candidate = fromBlock + size - 1n;
    yield { fromBlock, toBlock: candidate < end ? candidate : end };
  }
}

Для начального100, конечного205 и размера50 ожидаются100–149,150–199,200–205. Последняя порция короче, но не создаёт пропуска. Эти числа являются заданными входами для ручного расчёта; генератор здесь не запускался. Такой расчёт полезно сделать до включения транспорта и базы.

Чтение двух типов события

Порция получает события публикации и деактивации выбранного экземпляра. Адрес и ABI берутся из контролируемой конфигурации. Ограничение по courseId допустимо, если индексируется один курс; для полного реестра читаются все его события.

export async function readRange(client, deployment, abi, range) {
  const common = { address: deployment.catalogAddress, abi, ...range };
  const logs = (await Promise.all([
    client.getContractEvents({ ...common, eventName: 'Published' }),
    client.getContractEvents({ ...common, eventName: 'Deactivated' }),
  ])).flat();
  return logs.sort((a, b) => a.blockNumber === b.blockNumber
    ? a.logIndex - b.logIndex
    : a.blockNumber < b.blockNumber ? -1 : 1);
}

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

Отказ порции

Если сервис сообщает превышение лимита, диапазон можно уменьшить по ограниченному алгоритму. Но нельзя бесконечно делить, повторять и скрывать отказ от клиента. Нижняя граница и число попыток известны заранее. При невозможности прочитать даже один блок сервис фиксирует пробел и останавливает продвижение checkpoint за него.

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

Параллельное чтение нескольких порций допустимо для производительности, но сохранение checkpoint требует непрерывного завершённого префикса. Если диапазон200–249 готов, а150–199 ещё отказал, курсор не перескакивает на249. Иначе восстановление начнётся после неполного места и потеряет события. В учебном варианте сначала выбираем последовательную загрузку, чтобы ясно увидеть гарантию.

Пустой диапазон

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

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

Отдельная политика актуальности

Верхняя граница может быть текущим head или более устойчивым уровнем по правилам выбранной сети. Это решение не должно спрятаться в размере порции. Порция50 относится к нагрузке транспорта, а отступ от head — к политике каноничности и подтверждения. Два параметра отвечают на разные вопросы.

Для упражнения распределите блоки100–205, затем задайте отказ средней порции и пустую последнюю. Объясните, куда допустимо продвинуть checkpoint и что будет прочитано после перезапуска. Ожидаемый итог — ограниченное чтение сохраняет непрерывность без ложной полноты. Следующий урок сделает курсор устойчивым. API описан в getContractEvents.

Диапазон как единица ответственности

Пусть требуется пройти блоки со 101 по 230. Порции выбранного размера заканчиваются на 150, 200 и 230. Последняя короче остальных, что нормально: включительный верхний предел относится к фиксированной цели прохода. Новые блоки 231 и дальше попадают в следующий цикл, а не заставляют текущий проход постоянно увеличивать свою задачу.

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

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

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

Пустая порция тоже фиксирует покрытие, если источник вернул полноценный успешный ответ для обоих типов событий.

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