Инженерный кейс
Мониторинг витрин в играх
Панель следит за внутриигровыми магазинами двух десятков игр: что появилось, что убрали, что подорожало. Изменения уходят подписчикам, история хранится по каждому товару.
- источника за одним контрактом
- 23
- промаха до пометки об удалении
- 3
- порог блокировки удалений
- 50%
Панель непрерывно следит за внутриигровыми магазинами двух десятков игр и показывает в одном окне, что появилось, что убрали и что подорожало. По каждому товару ведётся история, изменения уходят подписчикам в мессенджер. Главная сложность в том, что почти ни одна игра не отдаёт витрину добровольно: у каждой свой способ добычи, и все они спрятаны за одним интерфейсом.
Единый контракт прячет несопоставимые способы добычи
Все сканеры наследуют один абстрактный класс: обязательные имя игры и метод обхода, необязательные уборка и проверка доступности. Обёртка превращает любое исключение в результат со статусом «не удалось».
Способы получить витрину несопоставимы между собой: где-то открытый веб-магазин, где-то данные из кэша клиента, где-то разбор трафика, где-то распознавание экрана. Без общего контракта каждый источник протёк бы в расписание, обход и рассылку, и добавление игры означало бы правки в пяти местах. А обёртка гарантирует, что падение одного сканера не роняет весь цикл.
Товар не удаляется по первому исчезновению
У каждой записи есть счётчик промахов: пометка об удалении ставится только после трёх подряд успешных обходов, в которых товара не было. Возвращение сбрасывает счётчик.
Очевидная реализация «нет в новом обходе — удалён» ловит собственный хвост. Источники периодически отдают неполный список без видимых причин, и наивное сравнение порождает пары-призраки: товар помечается удалённым, а в следующем обходе тем же товаром «появляется» — и оба события улетают подписчикам как настоящие.
Массовое удаление блокируется, если обход выглядит неудачным
Перед пометкой считается доля пропавшего. Пустой ответ при известных активных товарах, исчезновение больше половины, а для нестабильных источников и меньшая доля — все удаления пропускаются с записью в журнал.
Истёкшая сессия, сбой сети или магазин на обслуживании дают пустой ответ. Без предохранителя один такой обход «удалит» весь магазин, разошлёт подписчикам сотню удалений, а следующий удачный разошлёт те же сто как новинки. Порог отделяет «магазин правда опустел» — редкость — от «обход не удался», и в сомнительном случае система молчит, а не врёт.
Призраки гасятся сопоставлением по имени и цене
Перед рассылкой списки «добавлено» и «удалено» пересекаются по паре «название и цена», и совпавшее выбрасывается из обоих.
Часть игр меняет внутренний идентификатор товара между обходами, оставляя сам товар прежним. По идентификатору это выглядит как «старый удалён, новый добавлен» — два ложных уведомления об одном неизменном товаре. Сопоставление по тому, что человек реально видит, убирает пару целиком.
В чат идут только платные изменения
Перед отправкой каждое изменение проверяется дважды: настоящие ли это деньги и человеческое ли это название, а не внутренний код. Самый первый обход в пустую историю не рассылает ничего.
Клиенту важны покупки за реальные деньги, а магазины полны наград за игровую валюту и служебных записей. Без фильтра первый же запуск разошлёт весь каталог сотнями строк, а дальше в чат посыплются внутренние коды, протёкшие из кэша.
Ход обхода показывается лентой, а не одним ответом
Тяжёлые сканеры отдают поток событий: шаг, ещё шаг, готово со сводкой.
Отдельный обход идёт от секунд до минут — запуск игры, ожидание загрузки, разбор. Обычный запрос-ответ повесил бы вкладку на это время без признаков жизни, и любой промежуточный таймаут оборвал бы его как ошибку. Лента показывает, что процесс идёт, и на каком шаге он встал, если встал.
Лёгкие и тяжёлые сканеры разведены по паузам
Сначала быстрые запросы с короткой паузой, потом тяжёлые, которым нужно устройство, с паузой на порядок больше и отдельной паузой после полного круга.
Тяжёлые конкурируют за одно физическое устройство: гнать их вплотную — гарантированные конфликты. Лёгкие страдать от этой очереди не должны, поэтому идут первыми.
Метка «новое» живёт до реального изменения, а не до следующего обхода
В данных хранятся два времени: когда последний раз обновляли и когда последний раз что-то менялось. Метка в интерфейсе привязана ко второму.
Привяжи её к обходу — и она исчезнет через один цикл: человек, зашедший через час, изменений уже не увидит. Метка должна отмечать событие, а не факт опроса.
При обновлении сессии порядок уборки перевёрнут
Когда сохранённая сессия протухает, система обновляет её сама. Критичен порядок завершения: сначала аккуратно возвращаются сетевые настройки устройства, с проверкой, что вернулись, и только потом завершается вспомогательный процесс. Плюс флаг «обновление уже идёт».
Очевидное «сделал дело — прибил всё разом» оставляет устройство в состоянии, где его запросы упираются в уже мёртвого помощника: любая следующая команда зависает, и чинится это руками. А без флага два параллельных обновления одной сессии затирали бы друг друга.
Python · FastAPI · httpx · protobuf · Telegram Bot API