Розробка системи повторних спроб (retry) для інтеграцій Bitrix24
Інтеграції падають. Зовнішній API повернув 503, мережа морнула, банківський сервіс ушов на регламентні роботи. Питання не в тому, впаде чи інтеграція, а в тому, що станеться після падіння. Система retry — це автоматичне відновлення: не пройшло зараз — повторимо через хвилину, годину, день. Якщо після N спроб все одно не пройшло — сповіщимо людину.
Принципи, які порушувати не можна
Ідемпотентність. Повторна спроба має дати той же результат, що й перша, без побічних ефектів. Якщо операція створює платіжне доручення в банку — повторний виклик не має створити друге. Для цього використовуємо idempotency_key (унікальний UUID операції) — банк або зовнішня система ігнорує дубль з тим же ключем.
Експоненціальна затримка (exponential backoff). Перша спроба — негайно. Друга — через 1 хвилину. Третя — через 4 хвилини. Четверта — через 16 хвилин. Це запобігає шторму повторних запитів при відновленні перевантаженого сервісу.
Jitter. До затримки додаємо випадковий компонент (±20%). Якщо тисяча операцій упала одночасно й усі повторюють з однаковою затримкою — отримуємо ще один шторм. Jitter розбиває пік.
Максимальне число спроб. Після N спроб (зазвичай 5–10) операція позначається як остаточно помилкова. Далі — ручне втручання.
Архітектура черги з retry
Для хмарного Bitrix24 (немає доступу до сервера) retry реалізується через:
- Агенти Bitrix (
\CAgent::AddAgent) — для нескладних сценаріїв з невеликим числом операцій - Зовнішній сервіс (окремий PHP/Node.js сервер) з Redis Queue або RabbitMQ
Для коробкового Bitrix24 — агенти або черга на основі інфоблока/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 |
Так |
| Timeout мережі | 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 (якщо дані потрібно скорегувати перед повтором)
- Масове повторення групи завдань
Інтеграція з Bitrix24
При остаточній помилці або при перевищенні порогового числа помилок за період — сповіщення відповідальній особі у Bitrix24:
\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 днів |
| Сповіщення | Інтеграція з Bitrix24 IM | 1–2 дні |
| Моніторинг | Метрики, дашборд | 2–3 дні |
Система retry — обов'язковий компонент будь-якої production-інтеграції. Без неї кожен збій зовнішнього сервісу перетворюється на втрачені операції та ручну роботу з їх відновлення.







