Автоматизированный скрипт ценен не тем, что он «один раз отработал», а тем, что его поведение предсказуемо в реальных условиях: при сетевых сбоях, таймаутах, лимитах API и временной недоступности внешних сервисов. Именно поэтому логирование и повторные попытки — это не дополнительные удобства, а базовая часть надежной архитектуры. Когда ваш парсер каждое утро обходит несколько досок объявлений и тянет данные о квартирах, любая нестабильность внешнего источника может обрушить весь процесс. Без нормального логирования вы будете гадать, что случилось, а без retry — вручную перезапускать задачу после каждого сбоя.
Зачем вообще нужны логирование и retry
В простом скрипте ошибка часто выглядит как «что-то упало». В рабочем сервисе этого недостаточно: нужно понять, что именно произошло, на каком шаге, сколько раз повторялось и чем закончилось. Без этого невозможно быстро отличить случайный сбой от системной проблемы. Например, если ваш агрегатор объявлений перестал приносить свежие объекты, вы должны сразу видеть, что API одной из досок начал стабильно отвечать 503, а не грешить на собственный код.
Повторный запуск задач решает другую проблему: многие ошибки временные. В моей практике чаще всего встречаются такие сценарии:
- API ответил 429 из-за rate limit — вы превысили допустимую частоту запросов к доске объявлений или сервису застройщика;
- база данных риелторской CRM на секунду потеряла соединение;
- внешняя служба отдала 503 — например, временная перегрузка сервера с объявлениями;
- DNS-запрос не успел отработать из-за перегруженного роутера в офисе;
- задача упала из-за кратковременного таймаута при загрузке фотографий или планов этажей.
В таких случаях правильнее не завершать процесс сразу, а сделать ограниченное число попыток с задержкой между ними. Иначе вы будете вручную перезапускать скрипт после каждого временного сбоя, а время на рутину в проектах с недвижимостью стоит дорого.
Какие ошибки стоит повторять, а какие — нет
Не все исключения одинаковы. Ошибка в логике программы не исправится повтором, а сетевой сбой часто исправится. Это правило я вывел еще на первых парсерах: повторять запрос к API при ошибке валидации бессмысленно, а вот при timeout — обязательно.
Обычно стоит повторять
- временные сетевые ошибки (разрыв соединения, переполнение буфера);
- timeout — внешний ресурс не успел ответить за отведенное время;
- 429 Too Many Requests — превышен лимит запросов, нужно подождать;
- 502/503/504 от внешнего сервиса — временная недоступность шлюза или сервера;
- кратковременную недоступность БД или очереди;
- сбои при чтении внешнего API, особенно если это API застройщика или доски объявлений с нестабильным аптаймом.
Обычно не стоит повторять
- ошибки валидации данных — например, в ответе нет обязательного поля с ценой;
- неверный формат ответа — вместо JSON пришел HTML с капчей;
- отсутствие обязательного поля в объекте недвижимости;
- ошибки авторизации — токен протух, повтор с тем же ключом не поможет;
- баги в коде — повтор только умножит количество логов об одной и той же ошибке;
- 4xx, кроме 408 и 429 в отдельных сценариях: 400, 403, 404 повторять не нужно.
Практическое правило простое: повторяем только то, что имеет шанс пройти позже без изменения кода или входных данных. Если вы парсите конкретный объект и в ответе нет адреса, повтор через минуту не заставит сервер добавить это поле — это ошибка данных.
Базовая схема надежного скрипта
Хороший автоматизированный скрипт обычно строится по одной и той же логике. Эта же схема лежит в основе всех моих инструментов для работы с недвижимостью: от простого конвертера площадей до сложного агрегатора цен.
- Выполнить шаг (получить список объявлений, запросить детали, обновить цену в CRM).
- Если успех — записать результат в лог.
- Если ошибка временная — попробовать еще раз с задержкой.
- Если ошибка постоянная — остановиться.
- Если попытки исчерпаны — отправить сигнал о сбое (алерт в Telegram, email, webhook).
Эта схема помогает не только переживать сбои, но и не создавать бесконечные циклы повторов, которые могут добить внешнее API или заспамить логи. Когда у вас 20 парсеров одновременно обращаются к одной доске объявлений, отсутствие контроля над повторами быстро приведет к блокировке IP.
Что именно логировать
Лог должен отвечать не на вопрос «что-то сломалось», а на вопрос «что сломалось, где и в каком состоянии система была в этот момент». Особенно это важно, когда вы разбираете инцидент спустя несколько дней: по логам нужно восстановить цепочку событий без доступа к боевому серверу.
Минимальный набор полей
- время события (с точностью до миллисекунды, если анализируете гонки);
- имя задачи (например, parser_cian, sync_crm, price_updater);
- этап выполнения (запрос списка, получение деталей, сохранение в базу);
- номер попытки;
- тип ошибки (Timeout, HTTPError, ConnectionError);
- текст ошибки — обязательно без обрезания;
- длительность выполнения шага;
- итог: success / retry / failed;
- корреляционный идентификатор, если задача состоит из нескольких шагов (например, ID объекта недвижимости или trace ID запроса).
Пример полезной записи
Структурированная запись в JSON выглядит так:
{"time": "2024-01-15T09:12:33Z", "task": "parse_cian", "step": "fetch_ads", "attempt": 2, "error_type": "Timeout", "error": "connect timeout after 10s", "duration": 3.2, "result": "retry", "request_id": "8f3a2c1e"}
Такая строка полезнее, чем просто:
Error: connection failed
Потому что по ней можно понять, что именно происходило, и восстановить цепочку событий. Когда у вас в базе 500 тыс. объектов и несколько интеграций, без таких полей вы не найдете проблемный запрос за разумное время.
Какие уровни логов использовать
Разделение по уровням помогает не утонуть в шуме. Я часто вижу, как разработчики пишут все ошибки в ERROR, а потом не могут отделить реальную аварию от повторяющегося timeout. В системах агрегации недвижимости, где внешние источники нестабильны по определению, это особенно критично.
| Уровень | Когда использовать | Пример |
|---|---|---|
| DEBUG | Для отладки и детальной диагностики | параметры запроса, тело ответа, внутренние шаги |
| INFO | Для нормальных событий | старт задачи, успешная попытка, завершение |
| WARNING | Для временных проблем и повторов | retry после timeout, ответ 429 |
| ERROR | Для окончательного сбоя | все попытки исчерпаны |
| CRITICAL | Для аварийной ситуации | отказ ключевого сервиса, потеря данных |
Важно: не логируйте всё подряд на уровне ERROR. Иначе настоящие аварии потеряются среди повторяющихся временных сбоев. Если ваш парсер каждую ночь получает 429 от одного API и вы пишете это в ERROR, через неделю вы перестанете обращать внимание на ERROR вообще.
Как правильно организовать повторные попытки
Повтор должен быть управляемым. В продакшене плохая идея — просто делать while True до победы. Надежнее использовать понятные ограничения, особенно когда вы ходите в API досок объявлений, где за слишком частые запросы можно получить бан.
Основные параметры retry
- максимум попыток — сколько раз пробуем;
- базовая задержка — пауза перед повтором;
- backoff — рост задержки с каждой попыткой;
- jitter — случайная добавка к задержке, чтобы избежать синхронных повторов;
- фильтр исключений — что именно повторяем (только временные ошибки);
- таймаут на попытку — чтобы одна итерация не зависала навсегда.
Хорошая стратегия
- 3–5 попыток для внешних API;
- экспоненциальная задержка (1с, 2с, 4с, 8с…);
- случайный разброс задержки (jitter) в пределах ±20%;
- отдельная обработка 429 и 5xx: для 429 можно дополнительно учитывать заголовок Retry-After;
- немедленный stop на ошибках валидации и авторизации.
Когда вы опрашиваете бесплатный API застройщика, 5 попыток с экспоненциальной задержкой обычно достаточно, чтобы дождаться снятия временного лимита. Если превысить — рискуете получить блокировку ключа на сутки.
Пример логики retry на практике
Ниже — понятная схема поведения. Смысл здесь не в синтаксисе, а в архитектуре:
try:
result = call_external_api(url, timeout=10)
log.info("success", task=task_name, step=step, attempt=attempt, duration=...)
except TemporaryError as e:
if attempt < max_attempts:
log.warning("retry", task=task_name, step=step, attempt=attempt, error=e, delay=backoff(attempt))
sleep(backoff(attempt) + jitter())
retry()
else:
log.error("failed", task=task_name, step=step, attempt=attempt, error=e)
send_alert()
except PermanentError as e:
log.error("stop", task=task_name, step=step, error=e)
return
- временные ошибки идут в retry;
- постоянные ошибки завершают процесс сразу;
- финальный провал фиксируется в логах отдельно;
- каждая попытка оставляет след.
Такую схему я использую во всех сервисах: от парсера объявлений до синхронизации цен с риелторской CRM. Она проста, предсказуема и легко тестируется.
Экспоненциальная задержка: почему это важно
Если при сбое делать повтор через фиксированные 1–2 секунды, задача может:
- перегружать внешний сервис — особенно опасно для досок объявлений с чувствительной защитой от ботов;
- попадать в тот же самый проблемный момент — если сбой длится 30 секунд, повторы каждые 2 секунды бесполезны;
- создавать лавину повторов — когда десятки задач одновременно упали и начинают долбить API.
Экспоненциальная задержка решает это лучше:
- 1 секунда;
- 2 секунды;
- 4 секунды;
- 8 секунд;
- 16 секунд.
Часто к этому добавляют jitter — случайную прибавку к каждой задержке, например ±0.5 секунды. Это особенно полезно, если много задач упали одновременно, например после перезапуска сервиса или массового сбоя внешнего API. В парсинге недвижимости, где несколько воркеров одновременно опрашивают один источник, jitter помогает распределить нагрузку и не выглядеть как бот.
Что логировать при каждой попытке
Для повторяющихся задач полезно писать не только факт ошибки, но и контекст. Это позволяет потом анализировать частоту сбоев по каждому источнику и решать, стоит ли вообще с ним работать.
Рекомендуемый минимум
- номер попытки;
- причина повторного запуска (тип ошибки, код статуса);
- задержка до следующей попытки;
- адрес или идентификатор внешнего ресурса (URL, endpoint, ID объекта недвижимости);
- время ответа (latency);
- код ответа, если это HTTP;
- итог предыдущей попытки (retry, failed).
Хорошая практика
Если задача обращается к API, логируйте:
- endpoint;
- метод (GET, POST);
- статус-код;
- размер ответа;
- latency;
- trace id или request id (многие CDN и API-шлюзы возвращают свой идентификатор запроса).
Это особенно полезно в проектах, где много интеграций: парсеры, агрегаторы, CRM, уведомления, фоновые синхронизации. Когда вы разбираете, почему упала синхронизация с CRM риелтора, trace id от API застройщика может стать ключом к решению.
Где хранить логи
Выбор зависит от масштаба. Для локального парсера, который я запускал на ноутбуке, достаточно текстового файла. Для продакшн-сервиса с сотнями задач — централизованное хранилище.
Для небольшого скрипта
- обычный текстовый файл;
- ротация по размеру или дате (например, logs/parser_2024_01_15.log);
- отдельный файл на задачу, чтобы не смешивать потоки.
Для сервиса
- stdout/stderr в контейнерной среде (Docker, Kubernetes собирают логи автоматически);
- централизованный сбор логов (Elasticsearch, Loki, CloudWatch);
- структурированные JSON-логи (каждое поле отдельно, не строка-мешанина);
- поиск по request id и task id — это ускоряет диагностику в разы.
Если скриптов много, не храните всё в одном общем файле без структуры. Через пару недель это превращается в бесполезный архив, в котором невозможно найти ничего, кроме последней ошибки.
Частые ошибки при реализации
За годы работы с автоматизацией в недвижимости я наступил на все эти грабли. Разберем их, чтобы вы не повторяли.
1. Бесконечный retry
Опасен тем, что маскирует проблему и может бесконечно жечь ресурсы. Если API застройщика начал стабильно отвечать 500, бесконечный retry не починит сервер, а только забьет очередь и съест лимиты.
2. Одинаковый retry для всех ошибок
Это плохая идея: 400 и timeout — не одно и то же. Если вы будете повторять запрос с невалидным ID объекта, вы получите тот же 400, но потратите ресурсы и время.
3. Слишком шумные логи
Когда каждая попытка пишет по 10 строк, реальные инциденты теряются. В агрегаторе, который каждые 5 минут опрашивает 50 источников, шум быстро превысит полезный сигнал.
4. Отсутствие финального статуса
Если после пяти попыток задача молча завершается, разбираться потом почти невозможно. Вы не знаете, упала ли она навсегда, или просто кто-то убил процесс.
5. Нет корреляции между шагами
Если один процесс делает 6 запросов подряд (список объявлений, затем детали по каждому), а логов связи между ними нет, восстановить цепочку событий трудно. Обязательно добавляйте общий идентификатор задачи или объекта.
6. Повтор без таймаута
Иногда проблема не в ошибке, а в зависании. Если timeout не задан, retry может не сработать вообще: задача просто висит и не падает, а вы ждете. Для внешних API всегда ставьте таймаут 10–30 секунд.
Пошаговый чек-лист для внедрения
- Определите, какие ошибки временные, а какие постоянные.
- Добавьте таймауты на внешние запросы.
- Введите лимит попыток.
- Настройте экспоненциальную задержку.
- Добавьте jitter для массовых задач.
- Логируйте номер попытки и причину повторного запуска.
- Разделите INFO, WARNING и ERROR.
- Пишите финальный статус задачи.
- Настройте уведомление при исчерпании попыток.
- Проверьте поведение на тестовых сбоях (искусственно отключите сеть, верните 503).
Этот чек-лист я применяю к любому новому скрипту: будь то парсер цен или интеграция с CRM. После его выполнения вы получаете систему, которая не падает молча и не требует постоянного ручного контроля.
Когда retry лучше вынести отдельно от бизнес-логики
Если логика повтора размазана по коду, поддерживать её тяжело. Гораздо удобнее отделить:
- бизнес-логику задачи (что именно делаем: парсим, синхронизируем, отправляем уведомление);
- логику повторов (сколько раз, какие задержки, какие исключения);
- логирование (куда пишем, в каком формате);
- уведомления (кому и как сообщаем о фатальном сбое).
Такой подход упрощает тестирование и делает код читаемым. В итоге одна и та же схема retry может применяться к разным задачам: парсеру объявлений, синхронизации данных с риелторской CRM, отправке уведомлений о новых объектах, загрузке файлов с планами этажей. Вы пишете модуль retry один раз, а потом переиспользуете.
Практический ориентир для production
Для реального сервиса обычно хватает такой политики:
- 3 попытки для операций с API досок объявлений и застройщиков;
- 5 попыток для коротких фоновых задач (например, обновление цен, проверка доступности объекта);
- backoff с ростом задержки (экспоненциально или линейно);
- отдельный лог на каждую задачу (по task id);
- алерт после окончательного провала (email, Telegram, Slack);
- ручной или автоматический rerun только после анализа причины.
Если задача критична для бизнеса, одного retry недостаточно. Нужны:
- мониторинг ключевых метрик (количество успешных/провальных запусков, latency);
- алерты с контекстом (не просто «задача упала», а ссылка на логи);
- идемпотентность (повторный запуск не должен нарушать данные);
- защита от дублей (уникальные ключи операций);
- понятный механизм повторного запуска (кнопка «rerun» в интерфейсе или CLI-команда).
Идемпотентность: почему без нее retry опасен
Если задача при каждом запуске создает новый результат, повтор может нанести вред. В контексте недвижимости это особенно критично: представьте, что парсер из-за сетевого сбоя повторно обрабатывает одно и то же объявление и создает дубликат в базе. Или интеграция с CRM дважды отправляет заявку риелтору.
Чтобы этого избежать, задача должна быть идемпотентной: повторный запуск не должен ломать данные и не должен создавать лишний эффект.
Для этого используют:
- уникальные ключи операций (например, hash от URL объявления + дата);
- проверку уже обработанных объектов (SELECT перед INSERT);
- атомарные записи (INSERT … ON CONFLICT DO NOTHING);
- флаги статуса (pending → processed → failed);
- защиту от повторной отправки (идемпотентные ключи в API внешних сервисов).
Без идемпотентности retry превращается в источник дублей и рассинхронизации данных. Поэтому сначала проектируйте идемпотентность, потом добавляйте повторы.
FAQ
Сколько попыток retry считать нормой?
Для большинства задач достаточно 3–5 попыток. Больше — только если это оправдано бизнесом и есть backoff. Для API досок объявлений 3 попытки обычно хватает, чтобы пережить кратковременный сбой.
Нужно ли логировать каждую попытку?
Да, но без лишнего шума. Достаточно фиксировать старт, ошибку, задержку и итог. Не нужно писать полный дамп ответа на каждый retry — это замедлит систему и забьет логи.
Что лучше: повторять сразу или ждать?
Ждать. Даже небольшая задержка с backoff обычно полезнее, чем мгновенные повторы. Если внешний сервис перегружен, немедленный повтор только усугубит ситуацию.
Можно ли retry делать для любых ошибок?
Нет. Ошибки в данных, авторизации и логике повторять бессмысленно. Повторяйте только временные сбои, которые могут пройти без изменения кода.
Нужны ли структурированные логи?
Если задача живет дольше одного локального запуска — да. Структурированные логи (JSON) сильно упрощают поиск и анализ. Вы сможете фильтровать по task id, типу ошибки, времени и другим полям.
Вывод
Надежный автоматизированный скрипт — это не просто код, который «умеет запускаться». Это система, которая умеет объяснять, что произошло, и корректно переживать временные сбои. Логирование дает наблюдаемость, а retry — устойчивость. Вместе они превращают хрупкий скрипт в рабочий инструмент, которому можно доверять в ежедневной эксплуатации.
Если задача уже используется в продакшене, первым делом стоит проверить три вещи: есть ли таймауты, отделены ли временные ошибки от постоянных и можно ли по логам восстановить каждую попытку. Именно с этого начинается нормальная эксплуатация автоматизации. Без этого любой парсер, агрегатор или интеграция с CRM рано или поздно преподнесет неприятный сюрприз, и вы потратите часы на ручное расследование вместо того, чтобы заниматься продуктом.
