Разработка системы повторных попыток (retry) для интеграций Битрикс24
Интеграции падают. Внешний API вернул 503, сеть моргнула, банковский сервис ушёл на регламентные работы. Вопрос не в том, упадёт ли интеграция, а в том, что произойдёт после падения. Система retry — это автоматическое восстановление: не прошло сейчас — повторим через минуту, через час, через день. Если после N попыток всё равно не прошло — уведомим человека.
Принципы, которые нарушать нельзя
Идемпотентность. Повторная попытка должна давать тот же результат, что и первая, без побочных эффектов. Если операция создаёт платёжное поручение в банке — повторный вызов не должен создать второе. Для этого используем idempotency_key (уникальный UUID операции) — банк или внешняя система игнорирует дубль с тем же ключом.
Экспоненциальная задержка (exponential backoff). Первая попытка — немедленно. Вторая — через 1 минуту. Третья — через 4 минуты. Четвёртая — через 16 минут. Это предотвращает шторм повторных запросов при восстановлении перегруженного сервиса.
Jitter. К задержке добавляем случайный компонент (±20%). Если тысяча операций упала одновременно и все повторяют с одинаковой задержкой — получаем ещё один шторм. Jitter разбивает пик.
Максимальное число попыток. После N попыток (обычно 5–10) операция помечается как окончательно ошибочная. Дальше — ручное вмешательство.
Архитектура очереди с retry
Для облачного Битрикс24 (нет доступа к серверу) retry реализуется через:
- Агенты Битрикс (
\CAgent::AddAgent) — для несложных сценариев с небольшим числом операций - Внешний сервис (отдельный PHP/Node.js сервер) с Redis Queue или RabbitMQ
Для коробочного Битрикс24 — агенты или очередь на основе инфоблока/HL-блока.
Структура задачи в очереди:
{
"id": "uuid-v4",
"type": "bank_payment_create",
"payload": {
"deal_id": 1234,
"amount": 50000,
"idempotency_key": "pay-uuid-v4"
},
"attempts": 2,
"max_attempts": 5,
"next_run_at": "2025-03-13T15:30:00Z",
"status": "pending",
"last_error": "Connection timeout"
}
Таблица задач: integration_jobs в PostgreSQL или MySQL. Индекс по (status, next_run_at) — воркер выбирает задачи, готовые к выполнению.
Реализация воркера
Воркер — отдельный процесс, запускаемый по cron каждую минуту (или daemon через Supervisor). Алгоритм:
// Забираем пакет задач для выполнения (с блокировкой FOR UPDATE SKIP LOCKED)
$jobs = JobRepository::getPending(limit: 10);
foreach ($jobs as $job) {
try {
$job->markRunning();
$handler = HandlerFactory::create($job->type);
$handler->execute($job->payload);
$job->markSuccess();
} catch (RetryableException $e) {
// Временная ошибка — планируем повтор
$delay = $this->calcBackoff($job->attempts); // 2^attempts * 60 секунд
$delay += rand(0, (int)($delay * 0.2)); // jitter
$job->scheduleRetry($delay, $e->getMessage());
} catch (FatalException $e) {
// Бизнес-ошибка — не повторяем, уведомляем
$job->markFailed($e->getMessage());
$this->notify($job);
}
}
FOR UPDATE SKIP LOCKED — обязательно при нескольких воркерах. Без этого два воркера могут взять одну задачу и выполнить её дважды.
Классификация исключений
Критично правильно разделить ошибки на «повторить» и «не повторять»:
| Тип ошибки | Класс | Retry |
|---|---|---|
| HTTP 429 (Rate Limit) | RetryableException |
Да, большая задержка |
| HTTP 503 / 502 (Service Unavailable) | RetryableException |
Да |
| Таймаут сети | RetryableException |
Да |
| HTTP 401 (Unauthorized) | Специальный: обновить токен, потом retry | Да, 1 раз |
| HTTP 400 (Bad Request) | FatalException |
Нет |
| HTTP 422 (Validation Error) | FatalException |
Нет |
| Дубль операции (idempotency hit) | Успех | — |
Dead Letter Queue
Задачи, исчерпавшие лимит попыток, переходят в Dead Letter Queue (DLQ) — отдельную таблицу или очередь. DLQ — это не мусорная корзина, это список того, что требует внимания. Интерфейс для работы с DLQ:
- Просмотр ошибочных задач с полной историей попыток
- Ручной повтор после устранения причины ошибки
- Редактирование payload (если данные нужно скорректировать перед повтором)
- Массовое повторение группы задач
Интеграция с Битрикс24
При окончательной ошибке или при превышении порогового числа ошибок за период — уведомление ответственному в Битрикс24:
\CIMNotify::Add([
'MESSAGE_TYPE' => IM_MESSAGE_SYSTEM,
'TO_USER_ID' => $responsibleUserId,
'MESSAGE' => "Интеграция: операция #{$job->id} не выполнена после {$job->attempts} попыток. " .
"Ошибка: {$job->last_error}. Требуется ручное вмешательство.",
]);
Или через REST API im.notify.system.add, если уведомление отправляется из внешнего сервиса.
Мониторинг очереди
| Метрика | Что показывает |
|---|---|
pending_jobs_count |
Текущая нагрузка, размер невыполненных задач |
failed_jobs_count |
Накопленный долг ошибок |
avg_retry_count |
Среднее число попыток до успеха |
p99_execution_time |
Производительность воркера |
dlq_size_delta |
Рост или уменьшение DLQ |
Этапы разработки
| Этап | Содержание | Срок |
|---|---|---|
| Проектирование | Схема данных, классификация ошибок, стратегия backoff | 2–3 дня |
| Таблица задач и репозиторий | CRUD, блокировки, индексы | 2–3 дня |
| Воркер | Основная логика, обработка исключений | 3–5 дней |
| DLQ и интерфейс | Просмотр, ручной повтор | 3–5 дней |
| Уведомления | Интеграция с Битрикс24 IM | 1–2 дня |
| Мониторинг | Метрики, дашборд | 2–3 дня |
Система retry — обязательный компонент любой production-интеграции. Без неё каждый сбой внешнего сервиса превращается в потерянные операции и ручную работу по их восстановлению.







