Skip to content

feat(runner): приём событий через /runner/ - #83

Merged
bymyforge merged 1 commit into
funpayx:mainfrom
pointless5g:feature/runner-polling
Sep 28, 2026
Merged

bymyforge merged 1 commit into
funpayx:mainfrom
pointless5g:feature/runner-polling

Conversation

@pointless5g

Copy link
Copy Markdown
Contributor

Что сделано

Приём событий через POST /runner/, как у самого сайта FunPay. Раннер по-прежнему опрашивает FunPay раз в timer секунд, но вместо HTML-страниц (/chat/, /orders/trade, /users/<id>/, /orders/ и /chat/?node= на каждый изменившийся чат) отправляет один JSON-запрос на тик. Хендлеры и вызов start_polling не меняются. Основная логика в fpx/classes/runner/subclasses/_updates.py (около 280 строк), остальное точечные изменения и тесты.

  • Тик. POST /runner/ с объектами orders_counters и chat_bookmarks и тегами из прошлого ответа. Объект считается изменившимся, если FunPay прислал его с новым тегом. В простое изменений нет, и тик больше ничего не загружает.
  • Сообщения. Истории изменившихся чатов догружаются объектами chat_node, до 10 чатов в одном запросе. Новые сообщения отбираются по ID, поэтому до хендлеров доходят все, в том числе несколько за тик. Оповещение FunPay распознаётся по автору 0, а не по тексту, и в on_message не попадает (как после fix(chat_runner): не терять сообщения покупателя #81).
  • Заказы, покупки, отзывы. Страницы загружаются только по сигналу: сменились счётчики или в чат пришло оповещение (оплата, подтверждение, возврат, отзыв). Сигналов два: отзыв счётчики не меняет, а если за один тик один заказ оплатили, а другой подтвердили, счётчики остаются прежними. Загружается только то, на что есть хендлеры: без on_new_review профиль не открывается, без хендлеров покупок не открывается /orders/. Заказы сверяются по ID и статусу первой страницы продаж, смена даты в полночь событием не считается.
  • Порядок. Заказы тика уходят в хендлеры раньше сообщений: если on_new_order выставит состояние FSM, сообщение, пришедшее вместе с оплатой, обработается уже в нём.
  • Сбои. Если запрос истории упал, чат остаётся в очереди и загружается на следующем тике, без потерь и дублей. Если упала сверка страниц, сообщения тика всё равно доходят до хендлеров, а сверка повторяется на следующем тике. При прогреве страницы загружаются раньше первого /runner/, и первый тик сверяет их ещё раз, поэтому заказ, оплаченный во время старта, не теряется.
  • message.answer() больше не открывает страницу чата: имя чата (users-...) приходит в chat_node.

Прежний режим остался: start_polling(..., legacy_polling=True). Если он не нужен, уберу в этом же PR. Если новый режим лучше сначала сделать опциональным, переключу значение по умолчанию.

Нагрузка в простое: тик стоил 4 HTML-страницы (5, если у продаж есть вторая страница), теперь один JSON-запрос. При timer=3 это порядка 80 страниц в минуту против 20 запросов. Замерено на фейковом FunPay в обоих режимах. Новое сообщение добавляет 1 запрос на пачку до 10 чатов. Событие заказа добавляет страницу продаж и, как раньше, /orders/<id>/ на каждый изменившийся заказ.

Изменения поведения:

  • Отзывы и покупки проверяются, только если на них есть хендлеры.
  • Статус сверяется по первой странице продаж: смену статуса старого заказа не с первой страницы раннер не заметит.
  • В докстринге on_message убрал устаревшую оговорку: одинаковые сообщения подряд тоже доходят до хендлеров, в прежнем режиме после fix(chat_runner): не терять сообщения покупателя #81 тоже.

Как проверено:

  • Сквозные тесты (test_updates_e2e.py): настоящие Runner, RequestEngine и парсеры поверх httpx.MockTransport, фейковый FunPay отвечает на /runner/ по тегам. Сценарии: тик в простое стоит один запрос; два сообщения за тик; «Я оплатил заказ, где товар?»; новый чат; оплата и сообщение в одном тике; оплата и подтверждение за тик при неизменных счётчиках; полночь; ответ бота; отзыв; упавший запрос истории; 12 чатов загружаются двумя пачками; оплата во время прогрева; старые сообщения чата, которого не было в списке при запуске.
  • Юнит-тесты UpdatesRunner, разбора chat_bookmarks и chat_node на реальной разметке оповещения (из fix(chat_runner): не терять сообщения покупателя #81), первой страницы продаж и покупок, выбора режима.
  • Мутационная проверка: 8 намеренных поломок (размер пачки, сигнал по оповещениям, порядок заказов и сообщений, повторная сверка после прогрева, порядок прогрева, отсев оповещений, отсечка по ID при старте, сигнал счётчиков), каждую ловит хотя бы один тест.
  • На живом аккаунте продавца, два прогона по ~5 минут, раннер только слушал события. В простое тик стоил один POST /runner/ (21.8 запроса в минуту при timer=3). Сообщения покупателя дошли до on_message, автоответ продавца в хендлеры не попал. Оплата пришла в on_new_order с верным chat_id, подтверждение заказа и отзыв пришли в свои хендлеры. Отзыв нашёлся по оповещению в чате, счётчики он не меняет. Оповещения FunPay в on_message не попадали. Возврат и покупки вживую не проверяли.

Чек-лист перед отправкой

  • Код проверен локально и всё работает.
  • Написаны/обновлены тесты, и они проходят (запуск: pytest: 687 passed на 3.10, 3.11 и 3.12, стресс-тесты проходят).
  • Линтеры не ругаются (запуск: ruff check . и ruff format .).
  • Аннотации типов добавлены, тайп-чекер доволен (запуск: mypy fpx: новых ошибок нет, 8 было и до изменений). - Не обязательно

Вместо опроса HTML-страниц раннер получает события так же, как сам сайт
FunPay: один POST /runner/ на тик с объектами orders_counters и
chat_bookmarks. Объект считается изменившимся, если FunPay прислал его
с новым тегом, поэтому в простое тик стоит один JSON-запрос вместо
4-5 страниц.

- Истории изменившихся чатов догружаются пачками chat_node (до 10 чатов
  на запрос), новые сообщения отбираются по ID. Оповещение FunPay
  определяется по автору 0, а не по тексту.
- Страницы продаж, покупок и профиля загружаются только по сигналу:
  сменились счётчики или в чате появилось оповещение (оплата,
  подтверждение, возврат, отзыв), и только если на них есть хендлеры.
  Заказы сверяются по ID и статусу первой страницы и отдаются хендлерам
  раньше сообщений того же тика.
- Прежний режим остаётся: start_polling(..., legacy_polling=True).
@bymyforge bymyforge added this to the v0.9.0 milestone Sep 27, 2026
@bymyforge
bymyforge self-requested a review September 27, 2026 07:35
@bymyforge

Copy link
Copy Markdown
Collaborator

Привет, сладкий код, но к сожалению я не могу работать в воскресенье, это опасно для нервов

@bymyforge bymyforge left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Привет, всё отлично. Код просмотрел, протестировал на реальных условиях, все события ловит. Тесты тоже работают, красавчик

@bymyforge
bymyforge merged commit 9ae7d70 into funpayx:main Sep 28, 2026
4 checks passed
@bymyforge

Copy link
Copy Markdown
Collaborator

На будущее. Было бы здорово разбивать такие большие изменения на несколько логических коммитов. Так проще ревьюить и откатывать при необходимости. Для этого пулл реквеста уже можно оставить как есть, но давай на следующий раз так делать?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants