سياسة إعادة المحاولة — هي مجموعة من القواعد التي تحدد متى وكيف يعيد التطبيق المحمول محاولة استدعاءات الشبكة الفاشلة تلقائياً. مع الاتصالات غير المستقرة أو أخطاء الخادم المؤقتة، تعمل سياسة إعادة المحاولة المصممة جيداً على تحسين موثوقية التطبيق دون تدخل المستخدم. وفقاً لبحث أجرته Google Developer Relations (2025)، يقلل التنفيذ الصحيح لسياسة إعادة المحاولة من نسبة الطلبات المفقودة بنسبة 40–60% في التطبيقات المحمولة ذات عمليات الشبكة المتكررة.
النقاط الرئيسية
سياسة إعادة المحاولة — هي استراتيجية برمجية تحدد سلوك العميل عند فشل طلب الشبكة: أي الأخطاء يجب إعادة محاولتها، وكم مرة، وبأي تأخير، ومتى تتوقف عن المحاولة. في التطبيقات المحمولة، تعتبر سياسة إعادة المحاولة مهمة بشكل حاسم بسبب عدم استقرار الشبكات المحمولة والأعطال المؤقتة المحتملة على جانب الخادم.
تتضمن سياسة إعادة المحاولة الأساسية ثلاث معلمات: الحد الأقصى لعدد إعادة المحاولات (maxRetries)، التأخير الأولي (baseDelay)، واستراتيجية التباعد (backoff strategy). بالإضافة إلى ذلك، يمكن تحديد قائمة رموز حالة HTTP التي يجب أن تؤدي إلى إعادة محاولة، ومهلة زمنية لإحباط جميع المحاولات.
وفقاً لكتاب «Designing Data-Intensive Applications» لمارتن كليبنمان، 50% من الأعطال في الأنظمة الموزعة مؤقتة ويتم حلها بإعادة المحاولة. وهذا يجعل سياسة إعادة المحاولة واحدة من أكثر الطرق فعالية وغير مكلفة لتحسين تحمل الأعطال في التطبيقات المحمولة دون تغييرات في بنية الخادم.
الأخطاء المؤقتة (القابلة لإعادة المحاولة) هي النوع الوحيد من الأعطال التي يجب أن تستجيب لها سياسة إعادة المحاولة. تشمل مهلات الاتصال (SocketTimeoutException)، وعدم توفر الخادم مؤقتاً (HTTP 503, 502)، وأخطاء DNS. الأخطاء الدائمة — HTTP 400, 401, 403, 404 — لا معنى لإعادة محاولتها لأنها تشير إلى مشكلة في الطلب وليس في الشبكة أو الخادم.
وفقاً لمدونة AWS Architecture Blog، فإن التصنيف الصحيح للأخطاء إلى قابلة لإعادة المحاولة وغير قابلة لإعادة المحاولة هو أهم قرار عند تصميم سياسة إعادة المحاولة. إعادة محاولة طلب غير متضامن مع HTTP 401 قد تؤدي إلى حظر الحساب، وإعادة محاولة HTTP 400 قد تؤدي إلى إنشاء بيانات مكررة. قم دائماً بتكوين قائمة الرموز المراد إعادة محاولتها بشكل صريح.
الفاصل الزمني الثابت — أبسط استراتيجية: تتم كل إعادة محاولة بعد نفس الفاصل الزمني. على سبيل المثال، مع تأخير 2 ثانية، يعيد التطبيق محاولة الطلب بعد 2، 2، 2 ثانية. الفاصل الثابت بسيط في التنفيذ ويمكن التنبؤ به، لكنه يخلق حملاً موحداً على الخادم أثناء الأعطال الجماعية.
الفاصل الزمني المتزايد — يزداد التأخير خطياً مع كل إعادة محاولة: إعادة المحاولة الأولى بعد ثانية واحدة، الثانية بعد ثانيتين، الثالثة بعد 3 ثوانٍ، وهكذا. تمنح هذه الاستراتيجية الخادم مزيداً من الوقت للتعافي عند الأعطال المتكررة، لكنها لا تزال قابلة للتنبؤ للعديد من العملاء الفاشلين في وقت واحد.
| الاستراتيجية | صيغة التأخير | الوقت التراكمي (3 محاولات) | التطبيق |
|---|---|---|---|
| ثابت | delay = D | 3 × D | سيناريوهات بسيطة، مهلات محلية |
| متزايد | delay = N × D | 6 × D | تخفيف تدريجي للحمل |
| أسي | delay = D × 2^N | 7 × D | أعطال جماعية، خدمات سحابية |
| أسي + Jitter | delay = random(0, D × 2^N) | متغير | حمولة عالية، خدمات مصغرة |
يعتمد اختيار الاستراتيجية على طبيعة التطبيق. لمهام الخلفية لمزامنة البيانات على الأجهزة المحمولة، تعتبر الاستراتيجية الأسية مع jitter هي الأمثل — فهي توفر أعلى احتمالية للنجاح مع أقل حمل على الخادم وجهاز المستخدم.
التباعد الأسي — استراتيجية يتضاعف فيها التأخير بين إعادة المحاولات مع كل محاولة. إذا كان التأخير الأولي ثانية واحدة، فسيكون تسلسل التأخير 1، 2، 4، 8، 16 ثانية. وهذا يعطي الخادم وقتاً متزايداً أسياً للتعافي.
Jitter — انحراف عشوائي في التأخير يمنع طلبات إعادة المحاولة المتزامنة من عدة عملاء (مشكلة التزاحم). بدون jitter، سيعيد ألف عميل بنفس سياسة إعادة المحاولة محاولة الطلبات في وقت واحد، مما يخلق حملاً ذروياً على الخادم. يعمل Jitter على توزيع إعادة المحاولات على مدى زمني.
تسمح coroutines في Kotlin بتنفيذ التباعد الأسي مع 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 لامدا مع استدعاء شبكة وتنفذه مع تباعد أسي و jitter. إذا لم يكن الخطأ قابلاً لإعادة المحاولة أو تم تجاوز الحد الأقصى لعدد المحاولات، تعيد الدالة خطأ. يتم ضرب التأخير بعامل عشوائي من 0.5 إلى 1.5 لتوزيع موحد لإعادة المحاولات.
قاطع الدائرة — نمط تصميم يمنع طلبات إعادة المحاولة اللانهائية أثناء عدم توفر الخدمة لفترة طويلة. عندما يتجاوز عدد الأخطاء حداً معيناً، ينتقل قاطع الدائرة إلى الحالة OPEN ويعيد خطأ فوراً دون تنفيذ الطلب، مما يعطي الخادم وقتاً للتعافي.
في التطبيقات المحمولة، يكون قاطع الدائرة مفيداً بشكل خاص عند عدم توفر API بسبب الصيانة المخطط لها أو أعطال شبكة المشغل. بدونه، سيستهلك التطبيق البطارية وحركة المرور في محاولات إعادة لا نهائية، مما يضعف تجربة المستخدم ويقلل من عمر بطارية الجهاز.
لقاطع الدائرة ثلاث حالات: 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
}
}
}
يحتوي تنفيذ قاطع الدائرة في Kotlin على عداد أخطاء ومؤقت تعافي. تتحقق طريقة protect من الحالة الحالية قبل تنفيذ الطلب المغلّف وتحدّث عداد الأخطاء عند الفشل. بعد الوصول إلى failureThreshold، يتم رفض جميع الطلبات فوراً حتى انتهاء timeoutMs.
الشبكات المحمولة لها خصائص تجعل سياسة إعادة المحاولة مهمة بشكل خاص. التبديل بين Wi-Fi والبيانات المحمولة، فقدان الإشارة في مترو الأنفاق والأنفاق، الحظر المؤقت على مستوى المشغل — كل هذه السيناريوهات تؤدي إلى فشل الطلبات التي يمكن معالجتها بنجاح عن طريق إعادة المحاولة.
على Android، توفر مكتبة Retrofit و OkHttp آلية إعادة محاولة مدمجة من خلال Interceptor. على iOS، يتم حل المهمة عبر URLSessionConfiguration والتفويض المخصص. للتطوير عبر المنصات، يتضمن Ktor (KMP) دعماً مدمجاً لإعادة المحاولة باستراتيجيات قابلة للتكوين.
Combine — إطار عمل Apple للبرمجة التفاعلية. يكرر عامل retry في Combine publisher عدداً محدداً من المرات عند حدوث خطأ، لكنه لا يسمح بتكوين التأخير بين إعادة المحاولات. لسياسة إعادة محاولة كاملة، يتم استخدام مزيج مخصص من 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 بتنفيذ التباعد الأسي من خلال استدعاءات متكررة مع تناقص العداد ومضاعفة التأخير. يُنشئ عامل delay فاصل زمني بين إعادة المحاولات، بينما يعترض catch الخطأ ويقرر ما إذا كان سيعيد المحاولة أو يعيد الفشل.
الخطأ الأول — إعادة محاولة الطلبات دون التحقق من التناضح. إذا قام الخادم بإنشاء مورد ولكن لم يُرجع تأكيداً بسبب فشل الشبكة، فإن إعادة المحاولة ستُنشئ نسخة مكررة. لطلبات POST، استخدم دائماً مفتاح التناضح (Idempotency-Key) في الترويسة أو انتقل إلى إعادة المحاولة فقط لـ GET و PUT و DELETE.
الخطأ الثاني — إعادة المحاولة إلى ما لا نهاية. حدد دائماً عدداً أقصى من المحاولات (3–5 للتطبيقات المحمولة) ومهلة زمنية إجمالية لجميع المحاولات. إعادة المحاولة اللانهائية تستهلك البطارية وتُنشئ حملاً طفيلياً على الخادم، خاصة أثناء ترحيل قواعد البيانات أو تغييرات API.
الخطأ الثالث — تجاهل سياق التطبيق. إذا أغلق المستخدم التطبيق أو انتقل إلى الخلفية، يجب إلغاء سياسات إعادة المحاولة النشطة بشكل صحيح. استخدم coroutines مع SupervisorScope أو Combine مع دورة حياة واجهة المستخدم للإلغاء التلقائي لإعادة المحاولة عند إغلاق الشاشة.
الخطأ الرابع — عدم تسجيل محاولات إعادة المحاولة. بدون تسجيل، لن تعرف عدد الطلبات التي تمت إعادة محاولتها، وما الأخطاء التي حدثت، ومدى فعالية سياسة إعادة المحاولة الخاصة بك. أضف مقاييس: عدد إعادة المحاولات، النجاح بعد إعادة المحاولة، توزيع التأخيرات. ستساعد هذه البيانات في ضبط المعلمات المثلى للاستراتيجية لتطبيقك المحدد.
الأسئلة الشائعة
العدد الأمثل لـ إعادة المحاولات هو 3–5 محاولات لمعظم السيناريوهات. للمزامنة في الخلفية، 5–7 محاولات مقبولة؛ للطلبات التفاعلية (مثل إرسال نموذج)، لا يزيد عن 3. عدد أكبر من إعادة المحاولات لا يزيد من احتمالية النجاح ولكنه يستهلك بطارية المستخدم وبياناته.
التباعد الأسي — هو مضاعفة التأخير بين محاولات إعادة المحاولة: ثانية واحدة، 2، 4، 8، 16 وهكذا. إذا كان الخادم محملاً بشكل زائد، فإن التوقف القصير بين إعادة المحاولات الأولى يسمح له بالاستجابة بسرعة، بينما التوقف المتزايد مع كل إعادة محاولة لاحقة يعطي الخادم مزيداً من الوقت للتعافي.
أعد محاولة فقط الأخطاء المؤقتة: 408 (Request Timeout)، 429 (Too Many Requests)، 502 (Bad Gateway)، 503 (Service Unavailable)، 504 (Gateway Timeout). الأخطاء 4xx (باستثناء 408 و 429) تشير إلى مشاكل في العميل — إعادة محاولتها لا معنى لها وقد تكون خطيرة على بيانات المستخدم.
سياسة إعادة المحاولة تدير إعادة محاولة طلب واحد عند الفشل. قاطع الدائرة يدير حالة الاتصال بالخدمة: عند تراكم الأخطاء، يفتح الدائرة (OPEN) ويمنع الطلبات الجديدة. تعمل إعادة المحاولة على مستوى الاستدعاء الفردي، بينما يعمل قاطع الدائرة على مستوى التكامل مع الخدمة.
لـ اختبار سياسة إعادة المحاولة، استخدم NetworkInterceptor (OkHttp) على Android و URLProtocol (URLSession) على iOS لمحاكاة أعطال الشبكة. حدد المعلمات: تواتر الأخطاء، مدة عدم التوفر، ورموز الاستجابة. تتحقق اختبارات الوحدة مع MockWebServer (OkHttp) أو OHHTTPStubs (iOS) من منطق إعادة المحاولة دون شبكة حقيقية.
الخلاصة
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.
اقرأ أيضًا