Гонка на одной странице
Два парсера. Одна страница браузера. Race condition.
Это не метафора. Буквально: два вызова getReviews бежали параллельно с concurrency=2, и оба использовали одну и ту же session.page. Один открывает страницу -- второй перезаписывает. Один скроллит -- второй читает DOM, который уже изменился.
Результат: вместо 300 отзывов собирали 20. И выглядело так, будто отзывов и правда двадцать.
Как нашли
Не сразу. Парсер не падал. Не кидал ошибок. Просто возвращал мало данных. А «мало» -- это валидный результат. У некоторых бизнесов и правда двадцать отзывов.
Поняли, когда сравнили с реальностью. Browram -- 302 отзыва на Яндекс Картах. Парсер показывает 20. Значит, парсер врёт.
Это самый опасный тип бага: тихий. Не красный. Не loud. Просто неправильные данные, которые выглядят правильными.
Что исправили
Два изменения.
Первое: каждый вызов getReviews теперь открывает свою страницу. Не shared session.page, а отдельную. Параллельность сохраняется, но каждый парсер работает в своём контексте. Плюс hard-timeout 120 секунд -- если завис, умирает, а не блокирует очередь.
Второе: перешли на внутренний API Яндекс Справочника. Вместо скроллинга DOM и ожидания lazy-load -- прямые запросы на /sprav/api/{permalink}/reviews?page=N. Постраничный обход. Стабильный. Быстрый. И главное -- полный.
Результат: 127 и 302 отзыва вместо 20. Данные совпали с реальностью.
Concurrency -- это не параллелизм
Есть разница между «делать два дела одновременно» и «делать два дела одновременно на одном столе».
Concurrency -- это когда задачи чередуются. Параллелизм -- когда выполняются одновременно. В нашем случае было хуже: две задачи не просто чередовались, а писали в один ресурс без координации.
Классический race condition. Учебник. Но в учебнике он на двух потоках и shared memory. У нас -- на двух парсерах и shared browser page.
Решение то же, что и в учебнике: не шарить ресурс. Каждому -- своё.
Внутренний API
Отдельное удовольствие -- найти внутренний API. Яндекс Справочник не документирует свои эндпоинты. Но если открыть Network tab и поскроллить отзывы -- видно, куда летят запросы.
/sprav/api/{permalink}/reviews?page=1. Возвращает JSON. Пагинация. Без скроллинга, без ожидания рендера, без зависимости от DOM.
Внутренние API -- это как задняя дверь в ресторан. Не для посетителей. Но если знаешь, где она -- обслуживание быстрее.
Риск: Яндекс может изменить API без предупреждения. Но скроллинг DOM ломается ещё чаще. Выбираешь из двух нестабильностей менее нестабильную.
302 отзыва. Все на месте. Race condition убит. До следующего.