scrape_catalog_objects держал весь батч в одной незакоммиченной транзакции:
db.commit() стоял ПОСЛЕ блока `async with BrowserSession(...)`. Любой отказ после
цикла — исключение в BrowserSession.__aexit__, снятие Celery-таски, перезапуск
контейнера — обнулял все уже успешные UPDATE'ы.
Масштаб не гипотетический. Беговой режим здесь force=True («Загрузить все»), то
есть SQL без LIMIT:
объектов всего 13801
ни разу не скрейплено 13200 (95.7%)
последний catalog_scraped_at 2026-05-19
Это многочасовой прогон, где отказ в конце стоит всего.
Про достижимость честно: скрейпер сейчас не отрабатывает вовсе — DOM.РФ отдаёт
страницу «Доступ заблокирован [403]» с капчей (проба 20.08, подробности в #2443).
Чинить это стоит ДО разбана, а не после: первый же прогон будет как раз force=True
на 13200 объектов, то есть худший возможный случай.
Правка: commit после каждого успешного объекта. SAVEPOINT внутри
scrape_catalog_object к этому моменту уже снят, поэтому commit корректен.
Финальный commit оставлен — он закрывает транзакцию, которую могли autobegin'ить
неудачные итерации, и сохраняет прежнее поведение для вызывающих.
Тест не ходит в сеть и в БД: BrowserSession и scrape_catalog_object подменены,
сессия — счётчик вызовов. Против origin/main:
4 успеха + отказ на выходе → зафиксировано 0 раз вместо >=4 → падает
3 успеха из 6 → 1 фиксация вместо 4 → падает
пустой батч не трогает сессию — контроль, зелёный с обеих сторон
батч без успехов закрывает tx — контроль, зелёный с обеих сторон
Второй контроль стоит не для симметрии: он ловит «починку», которая выкинула бы
финальный commit, решив, что по-объектных достаточно.
Прогоны: tests/services/scrapers + tests/workers — 519 passed rc=0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>