Индексатор Ethereum: откат проекции и повторное построение
Оглавление курса «Web3: вход, смарт-аккаунты и индексатор» · Урок 17 из 30.
Обнаружение общей точки ещё не исправляет производное состояние каталога. Нужно убрать влияние неканоничных событий и построить новый согласованный результат. В этом уроке выберем простую стратегию пересборки проекции и сделаем переключение поколения атомарным. Результат — клиент не видит смесь старой ветви и частично обработанной новой.
Предполагаются anchors, каноничный журнал и чистая функция applyCatalogEvent. Программы, транзакции базы и узел не запускались. Начнём с полного replay небольшого учебного реестра: он проще для понимания, чем сложные обратные изменения. Для большого каталога стратегия оптимизируется отдельно.
Обратный переход не всегда очевиден
Для деактивации можно было бы вернуть active = true, но это предполагает знание прежнего состояния. Для публикации нужно восстановить предыдущий отпечаток, который новое событие уже заменило. Если обратные данные не сохранены, нельзя просто отменить запись арифметическим действием. Поэтому общий rollback должен опираться на историю или снимки, а не на угадывание.
В небольшом проекте безопаснее пересобрать проекцию из каноничного журнала до общей точки, затем применить события новой ветви. Чистая функция обеспечивает одну семантику для начальной загрузки и восстановления. Не создаём отдельный «почти такой же» проектор только для reorg: расхождение двух алгоритмов быстро приводит к скрытым ошибкам.
Сырой журнал старой ветви можно сохранить для расследования, пометив его неканоничным. Это отличается от видимой истории API, которая выбирает текущую каноничную часть. Данные прошлой ветви не обязаны исчезнуть физически, но не должны влиять на текущую карточку курса17.
Новое поколение проекции
Для пересборки создаём отдельный набор производных данных. Пока он не завершён, API продолжает показывать последнее завершённое поколение с отметкой восстановления или временно ограничивает актуальность по политике. После успеха переключается ссылка на новое поколение в одной транзакции.
export function rebuildCourses(events, initialCourse, applyCatalogEvent) {
const courses = new Map();
for (const event of events) {
if (!event.canonical) continue;
const before = courses.get(event.courseId) ?? initialCourse(event.courseId);
courses.set(event.courseId, applyCatalogEvent(before, event));
}
return courses;
}
Входной набор уже имеет правильный порядок и завершённую границу. Эта функция не сортирует неизвестные данные и не определяет каноничность сама. Если передать события новой и старой ветви с неправильно установленными флагами, она построит неправильный результат. Поэтому этап выбора истории предшествует вычислению и имеет собственные гарантии.
Map является учебным представлением памяти. Для серверного API данные записываются в таблицу поколения с ключами окружения, сети и курса. Факт записи каждого курса не означает завершение всего поколения. После полного успешного вычисления сохраняется граница блока и только затем меняется активный указатель.
Согласование с checkpoint
Новая проекция и новый checkpoint должны описывать один завершённый уровень. Нельзя переключить карточки на данные до150B, оставив курсор200A. После такого состояния следующий worker начнёт чтение не с того места. Транзакция переключения проверяет ожидаемое прежнее поколение и сохраняет обе части вместе.
События выше общей точки A прежней ветви исключаются из каноничного набора. Новые порции читаются с текущего источника и сохраняются идемпотентно. Поздний worker старого поколения не получает право обновлять активную проекцию. Условный checkpoint и идентификатор процесса защищают от повторного появления уже отвергнутой истории.
Если пересборка отказала на середине, активная ссылка не меняется. Частично созданное поколение помечается незавершённым и позже удаляется или продолжается по известной политике. Клиент не получает половину списка только потому, что часть inserts успела пройти. Незавершённость является отдельным состоянием сервиса.
Снимки для большого журнала
Полный replay удобен при сотнях событий, но может быть дорог при длинной истории. Тогда сохраняются проверенные снимки проекции на известных anchors. Восстановление начинается с подходящего общего снимка и применяет последующие каноничные события. Снимок имеет версию проекторной функции и blockHash, иначе старую схему можно принять за новую без понимания.
Удаление старых событий ограничивает способность восстановить данные. Политика retention должна согласовываться с доступными снимками и допустимой глубиной. Нельзя очистить журнал ради размера базы и продолжать обещать полный replay любого прошлого состояния. Ограничение публикуется в служебной архитектуре и при необходимости в API истории.
Сверка с getter выполняется на том же уровне, что активная проекция. Если чтение исторического состояния недоступно, факт этой дополнительной сверки остаётся неполученным. Не нужно объявлять состояние подтверждённым сравнением, которого endpoint не позволил выполнить. Сам алгоритм всё равно сохраняет своё известное происхождение.
Персональные последствия
Публичная публикация курса не является платёжным событием. Поэтому пересборка этого журнала не должна автоматически отнимать личный прогресс или заново выдавать покупку. Если производный сервис имеет внешние эффекты, им нужны отдельные компенсирующие действия и идентификаторы. Обычный replay публичной проекции не исполняет сеть и не повторяет HTTP-платежи.
Для нашей библиотеки кабинет может видеть, что курс временно неактивен по текущей истории. Его сохранённые отметки чтения остаются персональными данными. Эти области соединяются на уровне представления, не смешиваются в один журнал. Разделение помогает восстановить публичный источник без неожиданного изменения личности читателя.
Ожидаемая пересборка
Старая ветвь содержит PublishedA, Deactivated. Общая точка находится перед деактивацией, новая ветвь содержит PublishedB. После replay ожидается B иactive=true, а старая деактивация остаётся только архивным наблюдением. При отказе переключения API не выдаёт новое поколение как готовое.
Для упражнения опишите полный replay и вариант со снимком. Укажите, какие данные нужны в обоих случаях и какой атомарный момент делает новое состояние видимым. Эти критерии не проверялись запуском. Итог — восстановление является согласованной процедурой, а не исправлением одной карточки вручную. Транзакционная основа описана в PostgreSQL.
Побочные эффекты повторного построения
Replay проекции не должен повторно отправлять письма, создавать платёжные заявки или выдавать награды за старые события. Производное состояние строится заново, а внешние действия имеют собственные идентификаторы и отдельный журнал. Иначе обычное восстановление после сбоя станет повторной публикацией уведомлений для всей аудитории.