Инженерный кейс
Данные карточки товара по артикулу
Сервис отдаёт название, характеристики, цену и остаток товара маркетплейса по одному артикулу — с ключами доступа, тарифами и суточными квотами. Поверх — слежение за ценами конкурентов.
- артикулов за один запрос
- 100
- канала с разной свежестью
- 2
- попытки со сменой адреса
- 4
Сервис получает данные товарной карточки по артикулу и отдаёт их по HTTP: ключи доступа, тарифы, суточные квоты. Поверх — слежение за ценами конкурентов: группы «своё и чужое», история цен, оценка качества карточки. Партнёрских интерфейсов у площадок нет, поэтому данные берутся оттуда же, откуда их берёт сам сайт.
Карточка разрезана надвое — по доступности источника, а не для красоты
Неизменная часть — название, бренд, описание, характеристики — лежит в открытой раздаче и почти не режется. Цена, остаток и рейтинг приходят из защищённого канала, которому нужен чистый адрес. У каждой части своя свежесть: сутки против пяти минут.
Очевидное решение — бить в один главный адрес товара. Он же защищён жёстче всего: с серверного адреса там отказ, и карточка не отдаётся вообще — хотя четыре пятых полей лежат там, куда пускают. Разделение даёт надёжную половину всегда. Единая свежесть тоже не годится: описание не меняется месяцами, цена — каждый час.
Адрес неизменной части вычисляется из артикула, а не ищется
Хост и путь считаются арифметикой прямо из номера товара — без предварительного запроса к площадке.
Очевидный путь — открыть страницу товара и достать ссылку. Это лишний запрос ровно к тому слою, который банит: страница идёт через защиту, раздача — нет. Считая адрес локально, защищённого контура для неизменной части не касаемся вовсе. Цена решения — таблица диапазонов устаревает, поэтому для непопавших значений стоит запасной вариант: карточка отдаётся вместо жёсткого отказа.
Ответ всегда говорит, что случилось с каждой частью
Вместе с данными приходит пометка по каждому каналу: свежие данные, кэш, не найдено, отказ, пропущено.
Ключевой случай — товар существует, неизменная часть пришла, а защищённый канал ответил «нет такого». Наивный разбор запишет «товар не найден» — это молчаливая ложь: клиент видит пустую цену и решает, что артикула нет. Поверх этих данных строится аналитика: тихая подмена «заблокировано» на «нет цены» сместила бы все средние и медианы.
Пул адресов бросает «исчерпан» вместо пустого результата
Точки выхода раздаются по кругу, заблокированная уходит в остывание. Когда свободных нет, поднимается ошибка, а не возвращается пустота.
Очевидная реализация — «нет адреса, идём напрямую» или «вернём ничего». Первое сжигает собственный адрес машины ровно тогда, когда площадка банит агрессивнее всего. Второе неотличимо от «товара нет»: клиент получит успешный ответ с пустыми полями и запишет это в историю цен как факт. Честная ошибка останавливает нагрузку и позволяет повторить позже.
Остывание — это отметка времени, а не счётчик неудач
У адреса хранится момент, до которого его не трогать; доступность — сравнение с текущим временем.
Счётчик попыток пришлось бы обнулять по таймеру — то есть завести фоновую задачу ради одной переменной. Отметка времени восстанавливается сама при следующем обращении. И деталь: удачный запрос сбрасывает счётчик ошибок, но не снимает остывание — недавно забаненный адрес не стоит тут же нагружать из-за одной удачи.
Кэш общий на всех клиентов
Ключ кэша — площадка и артикул, без клиента. Запрос на сотню артикулов сначала берёт из кэша всё живое и идёт наружу только за недостающим.
Кэш на клиента выглядит естественнее, но ломает экономику: один популярный товар спрашивают десятки клиентов, и каждый запрос — это расход адреса и риск бана. Данные карточки публичны и одинаковы для всех, разделять там нечего.
Минутный лимит в памяти, суточная квота на диске
Ограничение частоты живёт скользящим окном в памяти. Суточная квота — счётчик в базе, обновляемый атомарно.
Асимметрия намеренная. Потеря минутного лимита при перезапуске не стоит ничего, а запись в базу на каждый запрос — лишняя работа на горячем пути. Суточная квота — это биллинг: её сброс при перезапуске означает бесплатные запросы столько раз, сколько было перезапусков.
Слежение разделяет «кто что отслеживает» и «что было с товаром»
Одна таблица — подписки клиента, другая — снимки цены по товару, без привязки к клиенту. Оценка качества карточки считается в момент снимка и туда же пишется.
История цены объективна, она не «чья-то». Хранить её по клиенту — значит для двух клиентов, следящих за одним конкурентом, держать две истории и делать два запроса. А оценка пишется сразу потому, что иначе график «как менялось качество карточки» потребовал бы заново скачать товар, которого может уже не быть в продаже.
Рекомендация по цене сравнивает с конкурентами, а не с рынком целиком
Свои товары исключаются из базы сравнения; средняя и границы считаются только по чужим.
Очевидная реализация — средняя по всей нише. Тогда собственная цена входит в базу, с которой сравнивается: у продавца с тремя карточками вердикт наполовину построен на его же ценах — поднял цену, средняя подтянулась, сервис сказал «нормально». Отдельно ловится крайний случай: конкурентов с ценой нет — это не «ты в рынке», а «сравнивать не с чем».
Python · FastAPI · SQLite · systemd