Resiliency - How to Build a Retryer

من ضمن المفاهيم المهمة التي ستغير من طريقة تفكير العديد من مهندسي البرمجيات هو مفهوم المرونة أو ما يعرف بالـ Resiliency وبالأخص مع الـ Downstream Dependencies ومن المحتمل أن العديد قد احتك بهذا وهو لايدري
Resiliency - How to Build a Retryer
Resiliency - How to Build a Retryer

في هذه الصفحة

من ضمن المفاهيم المهمة التي ستغير من طريقة تفكير العديد من مهندسي البرمجيات هو مفهوم المرونة أو ما يعرف بالـ Resiliency وبالأخص مع الـ Downstream Dependencies ومن المحتمل أن العديد قد احتك بهذا وهو لايدري.

في كثير من الأحيان ونحن نتعامل مع الـ Synchronous Communication Model أو ما يعرف بالـ Request – Response Model وهو شائع خصوصًا في تطبيقات الويب.

سترى أنك كثيرًا ما تقوم بمناداة Service تقوم بوظيفة معينة, ومن ثم تعتمد بشكل رئيسي على الـ Response العائد من الـ Request لهذه الـ Service حتى تستطيع تلبية اداء المهمة المطلوبة منك ..

فعلى سبيل المثال من الممكن أنه حين يستقبل التطبيق الذي تقوم بتطويره Request من نوع معين .. فأنه يقوم بالاعتماد على Service أخرى تقوم بعمل Verification أو أيًا يكن لتتأكد من استيفاء المطلوب قبل الرد أو ارسال الـ Response الخاص بك للـ Client ..

فكما نرى أن مسار الـ Request الآن أصبح كالآتي :

يقوم الـ Client بإرسال Request للـ Web Application الذي تقوم بتطويره ومن ثم يستقبل الـ Web Application هذا الـ Request ومن ثم يعتمد على Service أخرى فيقوم بإرسال Request آخر لها وينتظر الرد .. حتى يستكمل المطلوب ومن ثم يرد على الـ Client .. وهنا تكمن المشكلة ..

ماذا لو حدثت مشكلة في الـ Service التي تعتمد عليها ؟

ماذا لو تأخرت في الرد عليك لأي سبب من الأسباب ؟ ماذا لو كان هناك عطل ما في أحد الـ Servers أو الـ Network أثناء اعتمادك عليها ؟ فكما نرى هنالك العديد من الاحتمالات التي قد تتسبب في عدم رد هذه الـ Service عليك، أو التأخر في الرد عليك

وما هي بعض الاحتمالات التي قد تتسبب في ذلك ؟

١- من الممكن أن يكون هنالك مشكلة ما في الـ Network Latency وأن يصلك الرد متأخرًا فعلى سبيل المثال من الممكن أن تكون هذه الـ Service ليست Scalable بشكل كافي وتستقبل العديد من الـ Requests في هذا الوقت، ولكن بسبب طول الانتظار فقد يتسبب هذا في Request Timeout. وبالتالي سينتهي الـ Request قبل استقبال الرد من الـ Service.

٢- من الممكن أن تكون الـ Service حدث بها عطل ما وليست متاحة في هذا الوقت لأي سبب.

كيف لنا أن نتصرف في هذه الحالة ؟

هذا سؤال يجب عليك التفكير فيه، لإنه يختلف من نظام لآخر وباختلاف المطلوب منه. فإذن كنت تريد أن يكون النظام الذي تبنيه Resilient وقادر على الاستشفاء ومعالجة من هذا النوع من المشاكل بمعنى أنه قادر على الـ Recovery. فهنالك أكثر من طريقة تساعدك في تحقيق ذلك:

١- الـ Retryer

٢- الـ Circuit Breaker

ما هو الـ Retryer ؟

الـ Retryer هو مفهوم واضح من اسمه ويعني اعادة المحاولة بمعنى أنه بكل بساطة إذا واجهتني مشكلة ما وأنا أنادي الـ Service التي أعتمد عليها، فسأقوم بمنادتها عدد معين من المرات (يعتمد بشكل اساسي على نوع السلوك الذي أريده منه) حتى تستجيب وأحصل منها على رد.

ماذا لو فشلت بعد عدد معين من المرات أيضًا ؟

هذا السؤال يجب عليه أيضًا التفكير به أثناء تصميمك للنظام، فمن الممكن أن تفكر في أبسط شيء وهو أنك ستقوم بإرسال خطأ للـ Client بأنك فشلت في التعامل مع الـ Request , وأن هذا الفشل من ناحيتك أنت .. ومن الممكن أن تقوم بوسائل أخرى. فعلى سبيل المثال:

إذا كنت من الأساس تعتمد على الـ Asynchronous Communication Models أو Event Driven Architecture ولا ينتظر الـ Client منك Response في الحال .. فمن الممكن أن تعتمد على أن يكون لديك RetryableQueue ويكون الغرض من هذه الـ Queue هو انها تضم كل الـ Requests التي فشلت سابقًا وستعاود اعادة المحاولة عليها.

ومن الممكن أن تقوم أيضًا بحل يمزج الفكرتين مع بعضهما كأن تقوم بعمل Retry بسلوك معين عدد من المرات وبعد كل محاولاتك تعتمد على الـ Retryable Queue في وقت آخر.

كيف أقوم بعمل Retryer ؟

هنالك العديد من الـ Libraries والـ Frameworks التي تدعم الـ Retrying Technique والـ Failing Safe ولكن دعونا نقوم ببناء Retryer بسيط يدمج بين أنماط الـ Structure والـ Behavioural ويكون Generic وهذا من خلال جمال تطبيق الـ Design Patterns.

ما هي الـ Design Patterns التي سنعتمد عليها ؟

١- الـ Strategy

٢- الـ Builder

٣- الـ Factory

٤- الـ Observer

يمكنك رؤية الـ Code Snippets كاملًا على GitHub من هنا

مثال واقعي لتطبيق الـ Retryer على Google APIs لتجنب الـ RateLimits

var retryer = new Retryer.RetryPolicyBuilder()
                .handle(GoogleJsonResponseException.class, IOException.class)
                .withBackOff(1, MAX_SECONDS_RETRY, ChronoUnit.SECONDS, 2)
                .onRetry(() -> logger.warn("Retrying: " + request.getUriTemplate()))
                .onFailure(() -> logger.error("Retrying achieved maximum attempts with Google API Error: " + request.getUriTemplate()))
                .build();

        try {
            return retryer.run(() -> execute(request, ignoredResult , ignoredErrorCodes));
        } catch (Throwable e) {
            logger.error("Google API Error", e);
            throw new IOException(e);
        }
public enum RetryStrategy {
    RETRY_WITH_FIXED_DELAY,
    RETRY_WITH_EXPONENTIAL_BACKOFF,
    RETRY_WITH_MAX_RETRIES
}
public class RetryerExecutorFactory {

    public RetryerExecutor getInstanceOf(Retryer retryer) {
        switch (retryer.getRetryStrategy()) {
            case RETRY_WITH_MAX_RETRIES: return new MaxRetriesExecutor(retryer);
            case RETRY_WITH_EXPONENTIAL_BACKOFF: return new ExponentialBackOffRetriesExecutor(retryer);
            case RETRY_WITH_FIXED_DELAY: return new FixedDelayRetryExecutor(retryer);
            default: return null;
        }
    }
}
public interface RetryerExecutor {
    void run(CheckedRunnable checkedRunnable) throws Throwable;
    <ResultT> ResultT run(CheckedSupplier<ResultT> checkedSupplier) throws Throwable;
}

اشترك الآن بنشرة اقرأ-تِك الأسبوعية

لا تدع أي شيء يفوتك. واحصل على أحدث المقالات المميزة مباشرة إلى بريدك الإلكتروني وبشكل مجاني!