Skip to main content
Выдача доступа — та часть Omniflow, которая превращает деньги в доступ. Клиент платит, право на услугу фиксирует, что именно было куплено, а долговечная операция проталкивает это состояние в Remnawave. Это намеренно три разные записи: состояние платежа, состояние права на услугу и состояние Remnawave отказывают независимо друг от друга, и объединение их в одну означало бы, что сбой панели может потерять платёж. Omniflow никогда не пишет в хранилище Remnawave. Каждое изменение, которое он вносит в VPN-пользователя, — это HTTP-вызов к поддерживаемому API панели, описанный в Интеграция с Remnawave. Эта страница описывает механику посередине: запись операции, её ключ идемпотентности, расписание повторов, проход сверки, который чинит расхождения, и путь разбора отказавших операций, которым пользуется оператор, когда запуск перестал восстанавливаться сам.

Четыре записи

Право на услугу имеет UNIQUE (order_id), поэтому один заказ никогда не может породить два таких права, а CHECK (ends_at > starts_at) делает нулевой или отрицательный период непредставимым. Его статус — одно из pending, active, limited, disabled, expired, superseded или failed. Локально записываются только pending и superseded: pending — это промежуток между расчётом и первым успешным проталкиванием, superseded проставляется предыдущему праву на услугу той же подписки, как только выдано заменяющее. Каждое остальное значение читается обратно из Remnawave — ACTIVE становится active, LIMITED становится limited, DISABLED становится disabled, EXPIRED становится expired, а нераспознанный Omniflow статус становится failed, а не угадывается.
Право на услугу никогда не становится expired по таймеру. Фаза, которую видит клиент, вычисляется из ends_at в момент чтения; хранимый статус меняется только тогда, когда проход сверки прочитает EXPIRED обратно из панели. См. Подписки про лестницу фаз.

От оплаченного заказа к работающему доступу

Право на услугу никто не создаёт руками. Оно пишется путём расчёта внутри той же транзакции, которая помечает заказ оплаченным, и задание выдачи доступа вставляется в этой же транзакции. Транзакционная вставка задания и есть весь смысл: движение по книге проводок, право на услугу, уведомление оператору и долговечное намерение выдать доступ либо коммитятся все вместе, либо ни одно. Существует интеграционный тест, единственная задача которого — утверждать это свойство, потому что «мы вставляем задание сразу после коммита» — это соглашение о порядке, которое ломает падение процесса, а инвариант оно сломать не может. При успехе воркер записывает наблюдаемое состояние Remnawave в право на услугу и в подписку, закрывает все открытые расхождения по этому праву и — для create — помечает предыдущие права на услугу той же подписки как superseded и переводит заказ в fulfilled, если он всё ещё paid. Поэтому вторая подписка клиента никогда не выводит из обращения право на услугу первой.

Набор операций

Существует восемь операций, и это обеспечивается как CHECK в базе данных, так и белым списком в сервисе, который их ставит в очередь. Всё, что вне этого набора, окончательно отменяется, а не повторяется, потому что операция, которую воркер не умеет выполнять, от ожидания выполнимой не станет. Каждый PATCH и POST /api/users/ несёт оба лимита явно — trafficLimitBytes как 0 и hwidDeviceLimit как null, когда у тарифа их нет, — потому что Remnawave читает отсутствующее поле как «оставить как есть». Именно пропуск этих полей оставлял на пользователе лимит прежнего тарифа при переходе на безлимитный.

Что оплаченное изменение делает помимо окончания

Закрытие заказа помечает желаемое состояние для extension, renewal и upgrade флагом resetTraffic, и воркер сбрасывает счётчик трафика пользователя, как только новое состояние применено: клиент заплатил за новый период, и пользователь, которого Remnawave держит в LIMITED по расходу прошлого периода, возвращается в ACTIVE. purchase создаёт нового пользователя, которому сбрасывать нечего; downgrade сохраняет счётчик текущего периода. create против пользователя, которого Remnawave держит в DISABLED, включает его заново — если только право на услугу не в paused: пауза — это намеренное отключение, чьи часы остановил Omniflow, и снять его может только возобновление. Именно это делает оплаченное продление пользователя, который истёк и затем был отключён, действительно восстанавливающим доступ, а не только сдвигающим окончание. reconcile не делает ни того, ни другого: это починка состояния, за которое клиент заплатил, а не новая покупка, и операторский disable должен её пережить.

Детерминированное имя пользователя и подхват удалённого пользователя

Пользователь Remnawave для подписки называется omniflow_ плюс первые шестнадцать шестнадцатеричных символов UUID подписки. Установки, обновлённые с v0.4, дополнительно принимают устаревшее имя, производное от UUID клиента, для слота 1. Это предотвращает два разных отказа. Повторённый после потерянного ответа create не должен породить второго пользователя Remnawave, поэтому воркер сначала ищет имя пользователя и только потом создаёт его. А установка, где пользователи Remnawave уже были, должна их подхватить, а не продублировать, — именно поэтому слот 1 пробует обоих кандидатов. Вывод имени из подписки, а не из клиента, — это ровно то, что позволяет одному клиенту владеть несколькими независимыми пользователями Remnawave. Подписка в слоте 2 и выше без сохранённого сопоставления возвращает отсутствие удалённого пользователя, а не откатывается на пользователя уровня клиента, так что вторую подписку никогда нельзя спутать с удалённым состоянием первой. Операция, отличная от create, в этой позиции завершается с remote_user_not_found и повторяется: нельзя выполнить enable для того, чему доступ никогда не выдавался.

Идемпотентность

Каждая операция несёт ключ идемпотентности с ограничением UNIQUE в таблице. Вставка дублирующего ключа читает существующую операцию обратно, а не создаёт вторую. Одного лишь чтения строки обратно недостаточно, поэтому сервис затем перепроверяет параметры. Переиспользование ключа с другим правом на услугу, другой операцией, другим моментом вступления в силу, моментом окончания, объёмом трафика, лимитом устройств или набором сквадов отклоняется с сообщением «idempotency key was already used with different fulfillment parameters». Ключ — это обещание, что инструкция та же самая, и молчаливое применение другой инструкции под старым ключом — ровно тот отказ, ради предотвращения которого это обещание и существует. Ещё два отказа происходят до того, как что-либо ставится в очередь:
  • extend требует endsAt строго позже текущего окончания права на услугу. Продление на более ранний момент сократило бы то, что клиент купил.
  • Отрицательный объём трафика или лимит устройств отклоняется сразу.

Повторы, отсрочка и окончательный отказ

Запуск состоит из трёх шагов, и только средний обращается к Remnawave:
  1. Захват. Короткая транзакция уровня Serializable берёт SELECT … FOR UPDATE на строку операции, решает, нужно ли её выполнять вообще, читает всё, что понадобится вызовам, и переводит её в running, увеличивая счётчик попыток и записывая в next_attempt_at двухминутную аренду. Операция, уже находящаяся в статусе succeeded или cancelled, коммитится и ничего не делает.
  2. Вызов. Запросы к Remnawave — вне всякой транзакции. Пока панель отвечает, не удерживаются ни блокировка строки, ни транзакция, ни соединение из пула; раньше все три держались на протяжении до пяти последовательных запросов с таймаутом по десять секунд. Вызовы идемпотентны по построению: имя пользователя выводится из подписки и ищется до того, как что-либо создаётся.
  3. Запись. Вторая короткая транзакция записывает результат, но только если захват всё ещё принадлежит этому запуску: операция должна оставаться running с тем счётчиком попыток, который записал захват. Запуск, чья аренда истекла и которую перехватил другой, отбрасывает свой результат, а не перезаписывает более новый. Здесь же право на услугу перечитывается, поэтому право, выведенное из обращения, пока Remnawave отвечал, отменяется как entitlement_superseded, а не получает обратно active. Сбой сериализации в любой из транзакций повторяется на месте несколько раз; Remnawave при этом повторно не вызывается.
Поэтому два запуска не могут применить одну операцию одновременно. Запуск, нашедший операцию в running внутри её аренды, откладывается до конца аренды; запуск, нашедший аренду истёкшей — её владелец упал между захватом и записью, — перехватывает операцию со свежей попыткой. Операция, чьё право на услугу с тех пор стало superseded, отменяется до того, как у Remnawave что-либо спрашивают, с last_error_code = "entitlement_superseded" и строкой истории cancelled. Это защита от устаревшей сверки: reconcile, поставленная в очередь для старого права на услугу и выполненная после create продления, иначе протолкнула бы старое окончание поверх нового и записала бы active обратно на списанную строку. Это не отказ и никого не уведомляет — эта работа просто больше не нужна. create к тому же требует, чтобы заказ, доступ по которому он выдаёт, оставался в силе. Операция, чей заказ был возвращён до запуска выдачи доступа, отменяется с last_error_code = "order_refunded" (order_not_paid для заказа, который так и не был оплачен), у Remnawave ничего не спрашивают, а право на услугу — так и не выданное — выводится из обращения как superseded. Возврат, пришедший, пока create уже обращается к Remnawave, не может быть отменён успехом воркера: заказ переходит в fulfilled только из paid, поэтому возвращённый заказ остаётся возвращённым, а отзыв доступа по самому возврату забирает доступ обратно. Каждый отказ записывает операцию обратно в retrying с классифицированным кодом ошибки и зафиксированным временем следующей попытки 5s × 2^min(attempt, 8) — пять секунд, затем десять, двадцать и так далее до потолка в 1280 секунд. Очередь даёт заданию двенадцать попыток, после чего оно перестаёт повторяться само. Строка операции при этом сохраняет статус retrying и последний код ошибки, что важно, когда вы её ищете, — см. путь разбора отказавших операций. Брошенной она не остаётся: сканер оживления вставляет для неё новое задание, как только она простоит десять минут после зафиксированной следующей попытки, так что недоступность Remnawave дольше двенадцати попыток стоит задержки, а не ручного повтора. Ошибки классифицируются, а не хранятся свободным текстом, поэтому список отказавших операций читается с одного взгляда, а оператору никогда не отдаётся тело ответа провайдера. Два исхода повторами не являются:
  • Некорректное desired_state приводит к окончательному отказу. Операция переходит в failed, уведомление оператору ставится в очередь, а задание отменяется. Повтор сообщения, которое воркер не может разобрать, дал бы ту же ошибку разбора двенадцать раз подряд.
  • effectiveAt в будущем откладывает задание до этого момента вместо того, чтобы тратить попытку. Именно так понижение тарифа at_expiry дожидается конца текущего периода, не занимая воркер.
После трёх неудачных попыток уведомление оператору вида fulfillment_failure ставится в очередь внутри собственной транзакции воркера, так что оно существует ровно тогда, когда отказ стал долговечным. Оно дедуплицируется по operation:<operationID>, а значит одно проблемное право на услугу порождает одно сообщение независимо от числа повторов. Оно несёт идентификатор права на услугу, операцию, классифицированный код ошибки и статус — но никогда тело ответа Remnawave, ссылку на доступ или клиентский контент. Доставка описана в Уведомления оператору.

Когда Remnawave недоступен

Ничего не теряется и ничего не возвращается. Платёж уже записан, право на услугу уже существует, а операция лежит в retrying с last_error_code = "remnawave_unavailable" до тех пор, пока панель не ответит. Клиент видит подписку в фазе provisioning, а не ошибку, и клиентская веб-панель помечает свои живые показатели как degraded, а не выдаёт устаревшие числа за текущие. Затяжной сбой обычно вместо этого включает режим обслуживания, который меняет поведение намеренно.

Режим обслуживания удерживает выдачу доступа

Пока режим обслуживания активен, Blocks(ActionFulfillment) истинно, и воркер удерживает операцию вместо того, чтобы её выполнять. Проверка происходит внутри транзакции захвата, до перевода операции в running: счётчик попыток остаётся прежним, чтобы пауза не съедала повтор, воркер записывает maintenance_active, выставляет статус retrying со следующей попыткой через минуту, коммитится и откладывает задание на минуту. Удержание выдачи доступа без её потери означает, что сбой стоит задержки, а не права на услугу. Покупки в это же время заблокированы, поэтому новые деньги не входят в установку, которая не может выдать доступ; уже принятые деньги не трогаются, а расчёты, возвраты и история заказов продолжают работать.

Плановая сверка

Каждые пятнадцать минут процесс воркера ставит в очередь операцию reconcile для не более чем 500 прав на услугу, у которых reconciled_at пуст или старше пятнадцати минут. Это самовосстанавливающийся цикл: он обнаруживает расхождение, пересоздаёт удалённого пользователя, которого кто-то удалил, заново проталкивает желаемое окончание, трафик, лимит устройств и набор сквадов и обновляет observed_state, который читают панель и клиентские поверхности.
  • Ключ идемпотентности — reconcile:<entitlementID>:<bucket>, где корзина — это текущее время, усечённое до пятнадцати минут. Два прохода внутри одной корзины сходятся к одной операции.
  • Идентификатор корреляции — scheduler:<bucket>, именно так попытка сверки отличается в истории от попытки, вызванной покупкой.
  • Желаемое состояние пересобирается из самой строки права на услугу и не несёт effectiveAt, поэтому сверка никогда не откладывается.
  • Права на услугу в статусе pending не выбираются. Они всё ещё ждут своего первого create, а проход сверки не является его заменой.
  • Право на услугу, чей ends_at лежит больше чем на тридцать дней в прошлом, тоже не выбирается. Remnawave сам истёк пользователя в проталкиваемый момент окончания, и единственное, что сверка ещё могла бы сделать, — заново протолкнуть прошедшую дату или пересоздать пользователя, которого оператор удалил намеренно, — каждые пятнадцать минут, вечно. Тридцать дней перекрывают льготное окно и окно восстановления, после чего строка — история, а не состояние; клиент, вернувшийся позже, покупает новое право на услугу, которое выдаётся собственным create.
  • reconciled_at проставляется только после успешного применения, поэтому окончательно отказывающее право на услугу переставляется в очередь в каждой корзине и порождает одну новую строку операции каждые пятнадцать минут, пока не уйдёт за горизонт.
Интервал, размер пакета и горизонт зашиты в коде. Переменной окружения нет ни для одного из них.
Сверка и выдача доступа выполняются только в процессе воркера. Установка, где запущены API и бот без cmd/worker, принимает платежи и никогда ничего не выдаёт.

Оживление застрявших операций

Каждые пять минут тот же цикл воркера ищет операции, которые должны были выполниться и не выполнились: строки в pending, retrying или running, у которых и время создания, и зафиксированный next_attempt_at лежат больше чем на десять минут в прошлом. Для строки в running next_attempt_at — это аренда захвата, поэтому так находится захват, чей воркер упал, не записав результат; полученное задание перехватывает такой захват. Для каждой он вставляет задание fulfillment с теми же аргументами и теми же параметрами уникальности, что и исходное закрытие заказа. Именно вставка делает сканер безопасным для слепого запуска. River обеспечивает уникальность по идентификатору операции среди заданий в очереди, запланированных, выполняющихся, ожидающих повтора и завершённых, поэтому операция, чьё задание просто пережидает отсрочку, ничего нового не получает; задание получает только операция без живого задания — та, чей закрывающий процесс не имел вставщика заданий, или та, чьё задание очередь отбросила после двенадцатой попытки. Окно измеряется от попытки, которую воркер сам запланировал, а не от возраста операции, так что повтор на потолке отсрочки не принимается за сироту. Два следствия стоит знать:
  • Закрытие заказа внутри бота — оплата Telegram Stars, заказ, полностью покрытый кошельком, триал, активация подарка или кода доступа — вставляет своё задание в платёжной транзакции ровно так же, как API. Если какой-то процесс всё же закроет заказ без задания, этот сканер выдаст клиенту доступ примерно за пятнадцать минут, а не никогда.
  • Запуск, исчерпавший двенадцать попыток, повторяется снова, как только Remnawave возвращается, со свежим запасом попыток. Поэтому операция, которая продолжает отказывать, продолжает повторяться примерно каждые четверть часа, пока оператор её не отменит; уведомление fulfillment_failure дедуплицируется по операции, так что это даёт одно сообщение, а не по одному на каждое оживление.

Обнаружение расхождений

reconcile сравнивает желаемое состояние права на услугу с тем, что Remnawave содержит на самом деле, и записывает каждое расхождение строкой в entitlement_drifts с JSON-полями expected и observed. Remnawave — панель, которую может редактировать человек. Тот, кто вручную продлил пользователя, удалил его или изменил лимит устройств, порождает систему, которая противоречит сама себе, и клиент обычно замечает это раньше оператора. Запись расхождения до его исправления даёт оператору объяснение вместо молчаливой перезаписи. Одно открытое расхождение на пару (entitlement_id, kind) обеспечивается частичным уникальным индексом; повторное обнаружение обновляет expected, observed и detected_at, а не накапливает строки. Расхождение всегда вычисляется относительно того пользователя Remnawave, которым владеет эта подписка.
Пустой список расхождений — это нормально, а не доказательство того, что расхождений никогда не было. В успешном проходе сверки та же транзакция, которая открывает строки расхождений, закрывает их, как только исправляющий PATCH отработал. Расхождение остаётся открытым только пока исправляющая работа отказывает или Remnawave остаётся недоступен. Читайте /admin → System → Drift как «что сейчас невозможно починить», а не как журнал изменений панели.
external_edit присутствует в ограничении CHECK таблицы и в схеме API, но не пишется ни одним путём кода. Внешне изменённый пользователь на самом деле порождает конкретное расхождение по полю — expiry, traffic, device_limit, squads или status. Если само сравнение не удаётся по причине, отличной от 404, — ошибка чтения панели, — пишется строка истории со статусом retrying, и задание повторяется, ничего не применив, так что плохое чтение никогда не становится плохой записью. Закрытые строки расхождений старше APP_RETENTION_DRIFT_DAYS (по умолчанию 90) удаляются проходом очистки по сроку хранения. Открытые не удаляются никогда.

Изменения по запросу оператора

Из профиля клиента оператор с правом subscriptions.write может поставить в очередь изменение для одной подписки. Оно идёт через конвейер выдачи доступа, а не вызывает Remnawave напрямую, поэтому изменение оператора несёт тот же ключ идемпотентности, ту же политику повторов, то же обнаружение расхождений и ту же историю, что и изменение, порождённое покупкой. Клик в панели, который сам писал бы в Remnawave, был бы единственным изменением, которое сверка не смогла бы объяснить.
1

Откройте клиента

Перейдите в /admin/customers, найдите клиента и откройте его профиль. У каждой карточки подписки есть собственная панель действий.
2

Укажите причину

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

Выберите действие

Enable, Disable и Reset traffic больше ничего не требуют. Extend требует даты и времени «ends at». Set limits требует объёма трафика в байтах, лимита устройств или того и другого.
4

Прочитайте результат

Панель сообщает queued, а не «выполнено», потому что применяет изменение воркер. Следите за исходом в /admin → System → Jobs.
Маршрут — POST /v1/panel/customers/{customerID}/subscriptions/{subscriptionID}/operations, он возвращает 202 с {operationId, operation, status} и пишет событие аудита panel.subscription.<operation>. Его отказы стоит знать:
  • create и reconcile намеренно не предлагаются. Их разрешение дало бы оператору способ породить право на услугу без заказа за ним или устроить гонку с проходом сверки. Запрос такой операции возвращает 422 unsupported_operation.
  • Слот подписки, которому никогда не выдавался доступ, возвращает 409 not_provisioned.
  • Подписка всегда разрешается через клиента, названного в пути, поэтому оператор, угадавший идентификатор подписки, не может подействовать на клиента, которого он не смотрит.
  • Ключ идемпотентности включает идентификатор запроса, поэтому дважды отправленная форма переиспользует первую операцию вместо постановки в очередь второго изменения в Remnawave.
set_squads принимается API, но не имеет элемента управления в форме панели; панель предлагает enable, disable, reset traffic, extend и set limits. Для множества подписок сразу используйте массовые операции, описанные в Операции администратора; они ставят в очередь те же операции с ключом bulk:<operationID>:<position>, поэтому перезапущенный посреди пакета исполнитель не может продлить подписку дважды.

Путь разбора отказавших операций

Существуют две пересекающиеся поверхности, и разница между ними важна. /admin → System → Jobs работает со строками fulfillment_operations и загружается с status=failed. Каждая строка показывает время создания, операцию, число попыток, классифицированный код ошибки и идентификатор корреляции. С правом system.write оператор получает Retry и Cancel; вид истории перечисляет каждую попытку с её статусом, идентификатором корреляции и кодом ошибки. Ни одно из этих действий не требует причины, в отличие от операций над подпиской выше, и оба пишут событие аудита (panel.fulfillment.retried, panel.fulfillment.cancelled).
failed — это уже, чем «сдалось». Единственное, что пишет status = 'failed', — окончательный отказ, то есть операция, чьё желаемое состояние не удалось декодировать. Запуск, который просто исчерпал свои двенадцать попыток, остаётся в retrying с последним кодом ошибки. Запрашивайте GET /v1/panel/system/jobs?status=retrying, чтобы увидеть такие, и ?entitlementId=, чтобы увидеть всё по одному праву на услугу. И Retry, и Cancel принимают операцию в статусе retrying, так что действия работают в любом случае; просто фильтр по умолчанию её не покажет.
Операцию в статусе succeeded нельзя ни повторить, ни отменить, и это ограничение живёт в WHERE SQL-запроса, а не в Go, поэтому его нельзя обойти изменением обработчика. Ни одно из действий не может изменить ключ идемпотентности, и именно это делает повтор безопасным: это та же самая операция, которую воркер уже умеет выполнить ровно один раз. Сохранённые request_summary и response_summary не возвращаются ни одной из поверхностей. Они существуют для воркера и могут содержать ссылку на подписку. Ошибки уровня очереди проходят через редактор перед выходом из процесса, а аргументы задания сводятся в сводку, а не воспроизводятся дословно.
Кнопка Retry в панели переставляет строку; перезапускает её сканер оживления. POST /v1/panel/system/jobs/{id}/retry возвращает операцию в pending со свежим next_attempt_at и пишет событие аудита. Сам он задание в очередь не вставляет; это делает сканер оживления, как только строка простоит десять минут, так что повторной попытки стоит ждать примерно через пятнадцать минут. Повтор, который перезапускает задание немедленно, — это POST /v1/admin/jobs/{jobID}/retry на поверхности с операторским токеном.

Схема очереди

Задания выдачи доступа — это задания River в очереди critical, с MaxAttempts: 12 и уникальностью по аргументам задания, так что дублирующая вставка ничего не делает. Собственные таблицы заданий River поставляются вместе с миграциями Atlas, поэтому atlas migrate apply поднимает схему очереди вместе со всеми таблицами Omniflow, и вставке в очередь — включая ту, что происходит внутри транзакции расчёта, — есть куда писать. См. Задачи и очереди.

Что выдача доступа записывает, а что не записывает никогда

Каждая попытка пишет строку fulfillment_history с намеренно узкой сводкой: момент окончания, был ли настроен лимит трафика, был ли настроен лимит устройств, и число сквадов. Никогда идентификаторы сквадов, никогда ссылку на доступ, никогда тело ответа провайдера. Существует модульный тест, утверждающий, что сводка не содержит идентификаторов сквадов, потому что сводка — это то, что оператор читает чаще всего, и самое простое место для утечки учётных данных на экран. Метрики отдаются как omniflow_jobs_total{kind="fulfillment",outcome} и omniflow_job_duration_seconds; см. Наблюдаемость.

Конфигурация

Всё остальное на этой странице зафиксировано в коде: интервал сверки (15 минут), размер её пакета (500), её горизонт (30 дней после окончания), интервал сканера оживления (5 минут) и его окно простоя (10 минут), потолок попыток очереди (12), потолок отсрочки (1280 секунд), порог оповещения оператора (3 попытки), пауза при обслуживании (1 минута), таймаут запроса к Remnawave (10 секунд) и ограничение в 1 MiB на тело ответа Remnawave. Ничто из этого не вынесено в переменную окружения.

Смежные страницы

Подписки

Фазы прав на услугу, одновременность, арифметика повышения и понижения тарифа.

Интеграция с Remnawave

Адаптер, каждый его вызов и контрактный тест, который их фиксирует.

Задания и очереди

Виды заданий, необработанные сообщения, плановые циклы и проходы очистки.

Режим обслуживания

Что блокируется, что продолжает работать и как решает автоопределение.