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 است. تکرار یک درخواست غیر idempotent با HTTP 401 میتواند منجر به مسدود شدن حساب شود و تکرار HTTP 400 منجر به ایجاد دادههای تکراری. همیشه فهرست کدهای تکرار را به صراحت پیکربندی کنید.
Fixed interval — سادهترین استراتژی: هر تکرار پس از یک بازه زمانی یکسان انجام میشود. مثلاً با تأخیر ۲ ثانیه، برنامه درخواست را پس از ۲، ۲، ۲ ثانیه تکرار میکند. Fixed interval در پیادهسازی ساده و قابل پیشبینی است، اما در خرابیهای انبوه بار یکنواختی روی سرور ایجاد میکند.
Incremental interval — تأخیر با هر تکرار به صورت خطی افزایش مییابد: اولین تکرار پس از ۱ ثانیه، دومین پس از ۲، سومین پس از ۳ و غیره. این استراتژی در خرابیهای مکرر زمان بیشتری برای بازیابی به سرور میدهد، اما همچنان برای بسیاری از کلاینتهایی که همزمان خراب میشوند قابل پیشبینی است.
| استراتژی | فرمول تأخیر | زمان تجمعی (۳ تلاش) | کاربرد |
|---|---|---|---|
| Fixed | delay = D | ۳ × D | سناریوهای ساده، قطعیهای موضعی |
| Incremental | delay = N × D | ۶ × D | کاهش تدریجی بار |
| Exponential | delay = D × ۲^N | ۷ × D | خرابیهای انبوه، سرویسهای ابری |
| Exponential + Jitter | delay = random(0, D × ۲^N) | متغیر | بار بالا، میکروسرویسها |
انتخاب استراتژی به ماهیت برنامه بستگی دارد. برای وظایف پسزمینه همگامسازی داده در دستگاههای موبایل، استراتژی exponential با jitter بهینه است — بالاترین احتمال موفقیت را با حداقل بار روی سرور و دستگاه کاربر میدهد.
Exponential backoff — استراتژیای که در آن تأخیر بین تکرارها با هر تلاش دو برابر میشود. اگر تأخیر اولیه ۱ ثانیه باشد، دنباله تأخیرها ۱، ۲، ۴، ۸، ۱۶ ثانیه خواهد بود. این به سرور زمان نمایی فزایندهای برای بازیابی میدهد.
Jitter — انحراف تصادفی تأخیر که از درخواستهای مجدد همزمان از سوی کلاینتهای متعدد جلوگیری میکند (مشکل thundering herd). بدون jitter، هزاران کلاینت با Retry Policy یکسان به طور همزمان درخواستها را تکرار میکنند و بار اوجی روی سرور ایجاد میکنند. Jitter تکرارها را در زمان پخش میکند.
Coroutines در 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) یا حداکثر تعداد تلاشها exceeded شود، تابع خطا برمیگرداند. تأخیر برای توزیع یکنواخت تکرارها در ضریب تصادفی ۰.۵ تا ۱.۵ ضرب میشود.
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 و داده موبایل، از دست دادن سیگنال در مترو و تونلها، مسدودسازیهای موقت در سطح اپراتور — همه این سناریوها منجر به خرابی درخواستهایی میشوند که با تلاشهای مجدد با موفقیت مدیریت میشوند.
در اندروید، کتابخانه Retrofit و OkHttp از طریق Interceptor مکانیزم داخلی RetryPolicy را فراهم میکنند. در iOS، این کار از طریق URLSessionConfiguration و نمایندگی سفارشی حل میشود. برای توسعه بین پلتفرمی، Ktor (KMP) شامل پشتیبانی داخلی retry با استراتژیهای قابل تنظیم است.
Combine — فریمورک اپل برای برنامهنویسی واکنشگرا. عملگر 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 برگرداند.
اشتباه اول — تکرار درخواستها بدون بررسی idempotency. اگر سرور منبعی ایجاد کرده اما به دلیل خرابی شبکه تأییدیه برنگردانده باشد، درخواست مجدد یک کپی تکراری ایجاد میکند. برای درخواستهای POST همیشه از کلید idempotent (Idempotency-Key) در هدر استفاده کنید یا retry را فقط برای GET، PUT و DELETE اعمال کنید.
اشتباه دوم — تکرار بینهایت (retry forever). همیشه حداکثر تعداد تلاش (۳-۵ برای برنامههای موبایل) و یک بازه زمانی کلی برای همه تلاشها تعیین کنید. تکرارهای بینهایت باتری را خالی کرده و بار انگلی روی سرور ایجاد میکنند، به ویژه هنگام مهاجرت پایگاه داده یا تغییر API.
اشتباه سوم — نادیده گرفتن زمینه برنامه. اگر کاربر برنامه را بست یا به حالت پسزمینه رفت، Retry Policy فعال باید به درستی لغو شود. از coroutines با SupervisorScope یا Combine با چرخه عمر UI برای لغو خودکار تکرارها هنگام بسته شدن صفحه استفاده کنید.
اشتباه چهارم — عدم ثبت تلاشهای مجدد. بدون ثبت نمیدانید چند درخواست تکرار شده، چه خطاهایی رخ داده و Retry Policy شما چقدر مؤثر است. معیارهایی اضافه کنید: تعداد تکرارها، موفقیت پس از تکرار، توزیع تأخیرها. این دادهها به تنظیم پارامترهای بهینه استراتژی برای برنامه خاص کمک میکنند.
سؤالات متداول
تعداد بهینه تکرار — ۳-۵ تلاش برای اکثر سناریوها. برای همگامسازی پسزمینه ۵-۷ تلاش مجاز است، برای درخواستهای تعاملی (مثلاً ارسال فرم) — بیش از ۳ تلاش. تعداد بیشتر تکرارها احتمال موفقیت را افزایش نمیدهد، اما باتری و ترافیک کاربر را مصرف میکند.
Exponential backoff — دو برابر شدن تأخیر بین تلاشهای مجدد: ۱ ثانیه، ۲، ۴، ۸، ۱۶ و غیره. اگر سرور بیش از حد بارگذاری شده باشد، مکث کوتاه بین اولین تکرارها به آن اجازه میدهد سریع پاسخ دهد و مکث رو به افزایش با هر تلاش بعدی زمان بیشتری برای بازیابی به سرور میدهد.
فقط خطاهای موقت را تکرار کنید: 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) در اندروید و URLProtocol (URLSession) در iOS برای شبیهسازی خرابی شبکه استفاده کنید. پارامترها را تنظیم کنید: میزان خطا، مدت عدم دسترسی و کدهای پاسخ. تستهای واحد با MockWebServer (OkHttp) یا OHHTTPStubs (iOS) منطق تکرار را بدون شبکه واقعی بررسی میکنند.
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید