Політика повторних спроб — набір правил, які визначають, коли і як мобільний застосунок автоматично повторює невдалі мережеві виклики. При нестабільному з’єднанні або тимчасових помилках сервера грамотна політика повторів підвищує надійність застосунку без участі користувача. За даними дослідження Google Developer Relations (2025), правильна реалізація політики повторних спроб знижує відсоток втрачених запитів на 40–60% у мобільних застосунках із частими мережевими операціями.
Головне
Політика повторних спроб — це програмна стратегія, що визначає поведінку клієнта при збої мережевого запиту: які помилки варто повторювати, скільки разів, з якою затримкою і коли припинити спроби. У мобільних застосунках політика повторів критично важлива через нестабільність мобільних мереж і можливих тимчасових збоїв на стороні сервера.
Базова політика повторних спроб включає три параметри: максимальна кількість повторів (maxRetries), початкова затримка (baseDelay) і стратегія збільшення затримки (backoff strategy). Додатково може бути заданий список HTTP-статусів, на які слід реагувати повтором, і таймаут для переривання всіх спроб.
За даними книги “Designing Data-Intensive Applications” Мартіна Клеппмана, 50% збоїв у розподілених системах є тимчасовими і усуваються при повторній спробі. Це робить політику повторних спроб одним із найефективніших і найдешевших способів підвищення відмовостійкості мобільного застосунку без змін серверної архітектури.
Тимчасові помилки (retriable) — єдиний тип збоїв, на які політика повторних спроб повинна реагувати. До них належать таймаути з’єднання (SocketTimeoutException), тимчасова недоступність сервера (HTTP 503, 502) і помилки DNS. Постійні помилки — HTTP 400, 401, 403, 404 — повторювати безглуздо, оскільки вони вказують на проблему в запиті, а не в мережі або сервері.
За даними дослідження AWS Architecture Blog, правильна класифікація помилок на retriable та non-retriable — найважливіше рішення при проектуванні політики повторних спроб. Повтор неідемпотентного запиту на HTTP 401 може призвести до блокування облікового запису, а повтор HTTP 400 — до створення дублікатів даних. Завжди конфігуруйте список кодів для повтору явно.
Fixed interval — найпростіша стратегія: кожен повтор виконується через однаковий проміжок часу. Наприклад, при затримці 2 секунди застосунок повторює запит через 2, 2, 2 секунди. Fixed interval простий у реалізації та передбачуваний, але створює рівномірне навантаження на сервер при масових збоях.
Incremental interval — затримка збільшується лінійно з кожним повтором: перший повтор через 1 секунду, другий — через 2, третій — через 3 і так далі. Ця стратегія дає серверу більше часу на відновлення при повторних збоях, але все ще передбачувана для безлічі одночасно падаючих клієнтів.
| Стратегія | Формула затримки | Кумулятивний час (3 спроби) | Застосування |
|---|---|---|---|
| Fixed | delay = D | 3 × D | Прості сценарії, локальні таймаути |
| Incremental | delay = N × D | 6 × D | Поступове зниження навантаження |
| Exponential | delay = D × 2^N | 7 × D | Масові збої, хмарні сервіси |
| Exponential + Jitter | delay = random(0, D × 2^N) | varies | Високе навантаження, мікросервіси |
Вибір стратегії залежить від характеру застосунку. Для фонових задач синхронізації даних на мобільних пристроях оптимальна exponential стратегія з jitter — вона дає найбільшу ймовірність успіху при мінімальному навантаженні на сервер і пристрій користувача.
Exponential backoff — стратегія, при якій затримка між повторами подвоюється з кожною спробою. Якщо початкова затримка становить 1 секунду, то послідовність затримок буде 1, 2, 4, 8, 16 секунд. Це дає серверу експоненційно зростаючий час на відновлення.
Jitter — випадкове відхилення затримки, яке запобігає синхронним повторним запитам від безлічі клієнтів (thundering herd problem). Без jitter тисяча клієнтів з однаковою політикою повторних спроб буде повторювати запити одночасно, створюючи пікове навантаження на сервер. Jitter розносить повтори в часі.
Корутини Kotlin дозволяють реалізувати exponential backoff з jitter без блокування основного потоку. Функція retry з kotlinx-coroutines приймає умову повтору та блок з тілом запиту, автоматично керуючи затримками та кількістю спроб.
suspend fun RetryPolicy.executeWithRetry(
block: suspend () -> Result<T>
): Result<T> {
var lastError: Throwable? = null
repeat(maxRetries + 1) { attempt ->
try {
return block()
} catch (e: Exception) {
if (!isRetriable(e) || attempt == maxRetries) {
return Result.failure(e)
}
val delay = (baseDelayMs * (1 shl attempt))
.toLong()
val jitteredDelay = (delay * (0.5 + Random.nextDouble())).toLong()
delay(jitteredDelay)
lastError = e
}
}
return Result.failure(lastError!!)
}
Функція executeWithRetry приймає лямбду з мережевим викликом і виконує її з exponential backoff та jitter. Якщо помилка не підлягає повтору (не retriable) або перевищено максимальну кількість спроб, функція повертає помилку. Затримка домножується на випадковий коефіцієнт від 0.5 до 1.5 для рівномірного розподілу повторів.
Circuit Breaker — патерн проектування, який запобігає нескінченним повторним запитам при тривалій недоступності сервісу. Коли кількість помилок перевищує поріг, Circuit Breaker переходить у стан OPEN і негайно повертає помилку без виконання запиту, даючи серверу час на відновлення.
У мобільних застосунках Circuit Breaker особливо корисний при недоступності API через планове обслуговування або мережеві збої оператора. Без нього застосунок буде споживати батарею та трафік на нескінченні повторні спроби, погіршуючи користувацький досвід і скорочуючи час автономної роботи пристрою.
Три стани Circuit Breaker: CLOSED (нормальна робота, запити виконуються), OPEN (відмова, запити блокуються) і HALF_OPEN (пробний запит для перевірки відновлення). Після заданого таймауту в стані OPEN перемикач переходить у HALF_OPEN і виконує один запит — при успіху повертається в CLOSED, при невдачі — в OPEN.
class CircuitBreaker(
private val failureThreshold: Int = 3,
private val timeoutMs: Long = 30000
) {
private var state = State.CLOSED
private var failureCount = 0
private var lastFailureTime: Long = 0
suspend fun T.protect(block: suspend () -> T): T {
checkState()
return try {
val result = block()
onSuccess()
result
} catch (e: Exception) {
onFailure()
throw e
}
}
}
Реалізація Circuit Breaker на Kotlin містить лічильник помилок і таймер відновлення. Метод protect перевіряє поточний стан перед виконанням блокованого запиту та оновлює лічильник помилок при збоях. Після досягнення порогу failureThreshold всі запити негайно відхиляються до закінчення timeoutMs.
Мобільні мережі мають особливості, які роблять політику повторних спроб особливо важливою. Перемикання між Wi-Fi та мобільними даними, втрата сигналу в метро та тунелях, тимчасові блокування на рівні оператора — всі ці сценарії призводять до збоїв запитів, які успішно обробляються повторними спробами.
На Android бібліотека Retrofit та OkHttp надають вбудований механізм RetryPolicy через Interceptor. На iOS задача вирішується через URLSessionConfiguration та кастомне делегування. Для крос-платформенної розробки Ktor (KMP) містить вбудовану підтримку retry з налаштовуваними стратегіями.
Combine — фреймворк Apple для реактивного програмування. Оператор retry в Combine повторює паблішер вказану кількість разів при помилці, але не дозволяє налаштовувати затримку між повторами. Для повноцінної політики повторних спроб використовується кастомна комбінація catch та flatMap із затримкою.
extension Publisher {
func retryWithBackoff(
retries: Int = 3,
baseDelay: TimeInterval = 1.0
) -> AnyPublisher<Output, Failure> {
return self.catch { error -> AnyPublisher in
guard retries > 0 else {
return Fail(error).eraseToAnyPublisher()
}
return Just(())
.delay(for: .seconds(baseDelay), scheduler: DispatchQueue.main)
.flatMap { self.retryWithBackoff(
retries: retries - 1,
baseDelay: baseDelay * 2
) }
.eraseToAnyPublisher()
}
.eraseToAnyPublisher()
}
}
Розширення retryWithBackoff для Publisher в Combine реалізує exponential backoff через рекурсивний виклик із зменшенням лічильника та подвоєнням затримки. Оператор delay створює паузу між повторами, а catch перехоплює помилку та вирішує, чи намагатися знову чи повернути failure.
Перша помилка — повторювати запити без перевірки ідемпотентності. Якщо сервер створив ресурс, але не повернув підтвердження через збій мережі, повторний запит створить дублікат. Для POST-запитів завжди використовуйте ідемпотентний ключ (Idempotency-Key) у заголовку або переходьте на retry тільки для GET, PUT та DELETE.
Друга помилка — нескінченні повтори (retry forever). Завжди встановлюйте максимальну кількість спроб (3–5 для мобільних застосунків) і загальний таймаут на всі спроби. Нескінченні повтори розряджають батарею та створюють паразитне навантаження на сервер, особливо при міграції баз даних або зміні API.
Третя помилка — ігнорувати контекст застосунку. Якщо користувач закрив застосунок або перейшов у фоновий режим, активні політики повторних спроб повинні коректно скасовуватися. Використовуйте корутини з SupervisorScope або Combine з життєвим циклом UI для автоматичного скасування повторів при закритті екрана.
Четверта помилка — не логувати повторні спроби. Без логування ви не дізнаєтеся, скільки запитів було повторено, які помилки виникали і наскільки ефективна ваша політика повторних спроб. Додайте метрики: кількість повторів, успішність після повтору, розподіл затримок. Ці дані допоможуть налаштувати оптимальні параметри стратегії під конкретний застосунок.
Часті запитання
Оптимальна кількість повторів — 3–5 спроб для більшості сценаріїв. Для фонової синхронізації допустимо 5–7 спроб, для інтерактивних запитів (наприклад, відправка форми) — не більше 3. Більша кількість повторів не збільшує ймовірність успіху, але витрачає батарею та трафік користувача.
Exponential backoff — це подвоєння затримки між повторними спробами: 1 секунда, 2, 4, 8, 16 і так далі. Якщо сервер перевантажений, коротка пауза між першими повторами дозволяє йому швидко відповісти, а зростаюча пауза з кожним наступним повтором дає серверу все більше часу на відновлення.
Повторюйте лише тимчасові помилки: 408 (Request Timeout), 429 (Too Many Requests), 502 (Bad Gateway), 503 (Service Unavailable), 504 (Gateway Timeout). Помилки 4xx (крім 408 та 429) вказують на проблеми клієнта — повторювати їх безглуздо і може бути небезпечно для даних користувача.
Політика повторних спроб керує повторенням одного запиту при збої. Circuit Breaker керує станом з’єднання з сервісом: при накопиченні помилок він розмикає ланцюг (OPEN) і не допускає нові запити. Retry працює на рівні окремого виклику, Circuit Breaker — на рівні інтеграції з сервісом.
Для тестування політики повторних спроб використовуйте NetworkInterceptor (OkHttp) на Android та URLProtocol (URLSession) на iOS для симуляції збоїв мережі. Задайте параметри: частота помилок, тривалість недоступності та коди відповідей. Юніт-тести з MockWebServer (OkHttp) або OHHTTPStubs (iOS) перевіряють логіку повторів без реальної мережі.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також