Retry Policy — политика поновних захтева — скуп правила који одређују када и како мобилна апликација аутоматски понавља неуспеле мрежне позиве. При нестабилној вези или привременим грешкама сервера, добро осмишљена политика понављања повећава поузданост апликације без учешћа корисника. Према истраживању Google Developer Relations (2025), правилна имплементација Retry Policy смањује проценат изгубљених захтева за 40-60% у мобилним апликацијама са честим мрежним операцијама.
Главне тачке
Retry Policy — софтверска стратегија која одређује понашање клијента при неуспеху мрежног захтева: које грешке понављати, колико пута, са којим кашњењем и када прекинути покушаје. У мобилним апликацијама политика понављања је критично важна због нестабилности мобилних мрежа и могућих привремених грешака на страни сервера.
Основна Retry Policy укључује три параметра: максимални број понављања (maxRetries), почетно кашњење (baseDelay) и стратегију повећања кашњења (backoff strategy). Додатно се може задати листа HTTP статусних кодова на које треба реаговати понављањем и временско ограничење за прекид свих покушаја.
Према књизи „Designing Data-Intensive Applications" Мартина Клепмана, 50% грешака у дистрибуираним системима су привремене и отклањају се поновним покушајем. То чини Retry Policy једним од најефикаснијих и најјефтинијих начина за повећање отпорности на грешке мобилне апликације без промена серверске архитектуре.
Привремене грешке (retriable) — једина врста грешака на које Retry Policy треба да реагује. Оне укључују временска прекорачења везе (SocketTimeoutException), привремену недоступност сервера (HTTP 503, 502) и DNS грешке. Сталне грешке — HTTP 400, 401, 403, 404 — бесмислено је понављати, јер указују на проблем у захтеву, а не у мрежи или серверу.
Према истраживању AWS Architecture Blog, правилна класификација грешака на retriable и non-retriable је најважнија одлука при пројектовању Retry Policy. Понављање неидемпотентног захтева са 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) | променљиво | Високо оптерећење, микроуслуге |
Избор стратегије зависи од природе апликације. За позадинске задатке синхронизације података на мобилним уређајима, стратегија exponential са jitter-ом је оптимална — даје највећу вероватноћу успеха уз минимално оптерећење сервера и уређаја корисника.
Exponential backoff — стратегија у којој се кашњење између понављања удвостручује са сваким покушајем. Ако је почетно кашњење 1 секунда, низ кашњења ће бити 1, 2, 4, 8, 16 секунди. Ово даје серверу експоненцијално растуће време за опоравак.
Jitter — случајно одступање кашњења које спречава синхроне поновне захтеве од више клијената (проблем thundering herd). Без jitter-а, хиљаду клијената са истом Retry Policy ће понављати захтеве истовремено, стварајући вршно оптерећење сервера. 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.
Мобилне мреже имају карактеристике које чине Retry Policy посебно важном. Пребацивање између Wi-Fi и мобилних података, губитак сигнала у метроу и тунелима, привремена блокирања на нивоу оператера — сви ови сценарији доводе до грешака захтева које се успешно решавају поновним покушајима.
На Android-у, библиотека Retrofit и OkHttp пружају уграђени механизам RetryPolicy кроз Interceptor. На iOS-у, проблем се решава кроз URLSessionConfiguration и прилагођено делегирање. За међуплатформски развој, Ktor (KMP) садржи уграђену подршку за retry са подесивим стратегијама.
Combine — Apple-ов оквир за реактивно програмирање. Оператор retry у Combine-у понавља publisher одређени број пута при грешци, али не дозвољава подешавање кашњења између понављања. За потпуну Retry Policy користи се прилагођена комбинација 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-ја.
Трећа грешка — игнорисање контекста апликације. Ако је корисник затворио апликацију или прешао у позадински режим, активне Retry Policy треба правилно отказати. Користите корутине са SupervisorScope или Combine са животним циклусом UI за аутоматско отказивање понављања при затварању екрана.
Четврта грешка — невођење логова поновљених покушаја. Без логовања нећете знати колико захтева је поновљено, које грешке су се јављале и колико је ефикасна ваша Retry Policy. Додајте метрике: број понављања, успешност после понављања, расподелу кашњења. Ови подаци ће помоћи да подесите оптималне параметре стратегије за конкретну апликацију.
Често постављана питања
Оптимални број понављања — 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) указују на проблеме клијента — понављање их нема смисла и може бити опасно за податке корисника.
Retry Policy управља понављањем једног захтева при грешци. Circuit Breaker управља стањем везе са услугом: при нагомилавању грешака отвара коло (OPEN) и не дозвољава нове захтеве. Retry ради на нивоу појединачног позива, Circuit Breaker — на нивоу интеграције са услугом.
За тестирање Retry Policy користите NetworkInterceptor (OkHttp) на Android-у и URLProtocol (URLSession) на iOS-у за симулацију мрежних грешака. Подесите параметре: учесталост грешака, време недоступности и кодове одговора. Јединични тестови са MockWebServer (OkHttp) или OHHTTPStubs (iOS) проверавају логику понављања без стварне мреже.
Закључак
Развићемо мобилну апликацију под кључ
IT Sectr креира iOS и Android апликације за стартапе и предузећа од 2017. године. Саветоваћемо вас и предложити најбоље решење.
Прочитајте такође