Lightning: повторы, уведомления и пропущенные события
Оглавление курса «Bitcoin и Lightning: платежи на сайте» · Урок 25 из 30.
В предыдущем уроке подтверждённый расчёт получил чёткое предметное правило: верная связь, нужная сумма и атомарная выдача права. Теперь обеспечим получение этого факта, когда поток уведомлений прерывается или присылает повторы. Результатом станет процедура сверки по сохранённым payment hash и долговечный договор обработки. Она не обещает доставку каждого сообщения ровно один раз.
Узлы и потоки здесь не запускались. Показан собственный адаптер библиотеки, а не рабочий клиент LND. Версия будущей реализации должна фиксировать способы подписки и поиска. В статье важно понять восстановление наблюдения: создание нового invoice является другой операцией и не должно случайно выполняться при обычном сетевом повторе.
Уведомление ускоряет, сверка восстанавливает
Поток сообщает, что состояние могло измениться, и позволяет быстрее обновить заказ. Но соединение может закрыться, процесс — остановиться, а сообщение — прийти повторно. Сервер не должен хранить своё единственное знание о платеже в callbacks текущего соединения. После запуска он читает незавершённые инструменты из базы и сверяет их с источником.
В LND описаны поиск invoice по payment hash и поток с отдельными индексами добавления и расчёта. Их поведение определяется первичным RPC-интерфейсом LND. Не считаем, что любой поток автоматически воспроизводит все виды состояний во всех версиях: для незавершённых записей authoritative lookup остаётся самостоятельным путём.
Предположим, settlement курса 17 произошёл во время перезапуска сайта. Подписка после запуска может помочь воспроизвести наблюдение по сохранённому курсору, если договор это поддерживает. Но независимо от неё список unresolved инструмента даёт серверу payment hash для поиска. Право выдаётся по найденному факту, а не по тому, какое из двух соединений успело сообщить первым.
Ограниченная сверка
В собственной функции получаем инструменты небольшими порциями. Ошибка чтения одного счёта не преобразуется в canceled и не препятствует сохранению известных результатов других независимых заказов. При этом checkpoint списка относится к договору хранилища, а не вычисляется по случайному последнему HTTP-ответу.
def reconcile_page(store, adapter, page_limit=50):
instruments = store.list_unresolved(limit=page_limit)
results = []
for instrument in instruments:
try:
fact = adapter.lookup_verified(instrument)
except adapter.TemporaryUnavailable:
store.record_observation_problem(instrument.id, 'source-unavailable')
results.append((instrument.id, 'unknown'))
continue
result = store.apply_observation_atomically(instrument, fact)
results.append((instrument.id, result['state']))
return results
Методы store и adapter — договоры будущей реализации. Число 50 является выбранной учебной порцией, а не лимитом LND. Хранилище должно обеспечивать прогресс обхода: постоянно возвращать первые 50 зависших заказов и никогда не доходить до остальных нельзя. Можно использовать собственный курсор, расписание следующей сверки и очередь повторов.
lookup_verified проверяет источник, payment hash, сеть и профиль. Выбранное исключение TemporaryUnavailable не охватывает неправильную конфигурацию или несовместимую схему. Такие ошибки требуют отдельной остановки или диагностики. Широкий except Exception с молчаливым continue скрывал бы сломанную интеграцию под видом обычной сети.
Курсоры потока
Индексы создания и расчёта имеют разные назначения. Один lastIndex не описывает обе линии. Сохраняем их отдельно и только после долговечной обработки соответствующего наблюдения. Если процесс получил сообщение и остановился до commit, повтор после запуска допустим и обрабатывается идемпотентно.
Если поток прислал события вне ожидаемого порядка, нельзя просто взять максимальное значение и забыть меньшие неподтверждённые записи. Договор воспроизведения должен гарантировать покрытие либо приложение выполняет дополнительную сверку. Курсор показывает обработанную границу определённой линии, а не «самое большое число, когда-либо увиденное в JSON».
Переход paid и создание права сохраняются атомарно, как в предыдущей главе. Отправка письма — отдельный побочный эффект. Его можно связать с собственной outbox-записью и устойчивым идентификатором уведомления. Повтор settlement не должен снова выдавать доступ или создавать бесконечный поток одинаковых писем.
Повтор создания опаснее повторного чтения
Сбой lookup означает, что мы пока не получили наблюдение. Повтор этого чтения не создаёт нового платёжного обязательства. Сбой create может означать, что новый invoice уже существует, хотя ответ потерян. Поэтому одинаковая retry-обёртка вокруг обоих методов неверна без специального договора идемпотентности.
Для неизвестной подготовки сохраняем preparationId и предпринимаем определённый профилем поиск. Если поиск невозможен, интерфейс показывает ограничение, а оператор разбирает попытку. Новый инструмент выдаётся только как явное решение с учётом судьбы прежнего. Иначе два счёта могут быть оплачены по одному заказу.
Сторонний webhook также не считается достаточным доказательством только по форме JSON. Проверяем его подлинность согласно договору поставщика, защищаем от повторов, сопоставляем инструмент и при необходимости подтверждаем через чтение источника. Пользовательский запрос «я оплатил» может быть поводом запустить сверку, но не переводит заказ в paid самостоятельно.
Восстановление интерфейса
После перезагрузки браузер читает серверный статус своего заказа. Он не хранит собственную окончательную истину в localStorage. Если сервер знает paid, страница показывает доступ. Если сервер продолжает сверку, виден unknown или pending с последним временем достоверного источника. Счётчик ожидания страницы не отменяет платежи узла.
Публичный polling ограничен по частоте и полномочиям. Читатель не должен запускать дорогую полную сверку всех invoice одним запросом. Сервер может объединить несколько проверок одного инструмента и отдать сохранённый статус, сохраняя свежесть. Это снижает нагрузку без выдуманного обещания мгновенного ответа.
Ожидаемые случаи
Одно settlement приходит дважды: база создаёт одно право и возвращает согласованный результат повтору. Соединение закрывается перед обработкой: последующая сверка находит состояние по payment hash. Один lookup недоступен: заказ остаётся неизвестным, другие независимые наблюдения могут завершиться. Неправильный профиль вызывает диагностику, а не бесконечный тихий retry.
После перезапуска нет необходимости просить читателя оплатить заново только потому, что исчез браузерный таймер. Сервер восстанавливает исходные инструменты и применяет прежнюю политику. Таким образом мы получили устойчивое наблюдение, которое переживает повторы и пропуски, не создавая новые платежи под видом проверки статуса.
Последняя успешная сверка записывается для конкретного инструмента. Общий удачный запрос узла не снимает неизвестное состояние всех заказов. Это позволяет отличить единичный проблемный invoice от общего отказа источника и назначить ему отдельный ограниченный повтор.