الگوی Retry به برنامه کمک می کند که خطاهای گذرا و موقت را مدیریت کند

زمانی که یک سرویس خطای شبکه یا ارتباطی می­دهد، درخواست پس از وقفه ای یا بدون وقفه مجددا ارسال شود و این خطای گذرا باعث توقف عملیات سیستم نشود.

صورت مساله

زمانی که یک اپلیکیشن با یک سرویسی روی شبکه یا المانی روی cloud ارتباط برقرار میکند، احتمال دارد به صورت موقت با خطایی روی شبکه مواجه شود و یا به هردلیلی سرویس در آن لحظه قادر به پاسخگویی نباشد و خطای timeout و یا شلوغی شبکه رخ دهد.

این خطا ها عموما بعد از مدت زمان مشخصی بدون نیاز به دخالت برطرف می شوند. برای مثال پایگاه داده ای که در یک لحظه در حال پردازش درخواست های زیادی است ممکن است در زمان رسیدن درخواست جدید خطای timeout صادر کند و یا آماده پردازش درخواست جدید نباشد، اما پس از چند ثانیه این آمادگی را دارد که درخواست های جدید را پاسخ دهد.

راه حل

روی شبکه و cloud بروز خطاهای موقت غیر عادی نیست و چنین خطاهایی زیاد اتفاق می افتد بنابراین بهتر است برای بالا بردن پایداری سیستم این خطاها با استفاده از الگوی retry مدیریت شوند.

زمانی که یک اپلیکیشن در زمان ارسال درخواست به یک سرویس با خطا مواجه می شود می تواند خطا را با استراتژی های زیر مدیریت کند.

  1. صرف نظر(Cancel): زمانی که خطای رخ داده موقتی نباشد، تلاش دوباره در انجام عملیات هیچ تاثیری ندارد و مجددا خطا رخ می دهد. در اینجور مواقع اپلیکیشن باید عملیات را متوقف کند و خطا برگرداند. به عنوان مثال خطای لاگین به علت وارد کردن نام کاربری و / یا کلمه عبور نادرست حتی با 10 بار تکرار عملیات هم برطرف نمی شود و باید خطا بازگردانده شود.
  2. تلاش دوباره (Retry): زمانی که خطای رخ داده غیر معمول باشد و یا خطایی باشد که به ندرت اتفاق می افتد، مثلا عملیات انجام شود و پاسخ به درخواست دهنده نرسد، بدون وقفه درخواست می تواند تکرار شود، زیرا احتمال خیلی کمی دارد که با تکرار درخواست این خطا مجدد رخ دهد.
  3. تلاش دوباره پس از وقفه ای کوتاه (Retry after delay): زمانی که خطایی معمول تر مثل شلوغی شبکه یا خطای ارتباطی رخ می دهد، اپلیکیشن باید پس از وقفه ای درخواست خود را مجددا ارسال کند تا علت خطا برطرف شود.
فلوچارت الگوی retry

میزان وقفه ای که در نظر گرفته می شود، باید مناسب باشد. اگر instance هایی از اپلیکیشن که این درخواست را ارسال می کنند زیاد باشند، باید مدتی را در نظر گرفت که مثلا شلوغی شبکه بتواند کاهش پیدا کند وگرنه خود این درخواست های زیاد، باعث ادامه یافتن شلوغی شبکه و خطا می شوند.

اگر درخواست مجدد نیز منجر به خطا شد، اپلیکیشن می تواند پس از وقفه ای دوباره درخواست خود را ارسال کند. اگر خطا ادامه یافت در صورت نیاز می توان مدت وقفه را بیشتر کرد و دوباره درخواست را ارسال نمود.

شکل زیر فراخوانی یک سرویس را با استفاده از الگوی retry نمایش می دهد. اگر خطا پس از تعداد مشخصی درخواست مجدد برطرف نشود، اپلیکیشن دیگر آن را خطای موقت قلمداد نمیکند و آن را تحت عنوان یک خطا در سیستم مدیریت میکند.

در شکل زیر تلاش اول و دوم با خطا مواجه شد، اما در درخواست سوم پاسخ دریافت می شود.

درخواست های متعدد retry

اپلیکیشن ای که از الگوی retry استفاده میکند باید از یکی از سه استراتژی مطرح شده برای سرویس های مورد نیاز استفاده کند. استراتژی ها برای سرویس های مختلف متفاوت اند. برخی از محیط های برنامه نویسی الگوی retry را پیاده سازی کرده اند و برای هر سرویس می توان با مشخص کردن تعداد تلاش دوباره و یا وقفه بین ارسال درخواست ها از آنها استفاده نمود.

اپلیکیشن ها باید خطاها و جزئیات آن را لاگ کنند. اگر یک سرویس مرتبا در دسترس نیست، احتمالا بار روی آن زیاد است و نیاز به منابع بیشتری دارد.

نکات و ملاحظات الگوی retry

زمانی که میخواهیم از الگوی retry استفاده کنیم، نکات و موارد زیر باید مد نظر قرار گیرد.

  • در زمان استفاده از این الگو باید نوع بیزینسی که این الگو در آن مورد استفاده قرار میگیرد، بررسی شود. برای مثال در یک وبسایت اگر در زمان فراخوانی یک سرویس خطایی رخ دهد بهتر است پس از چند تلاش ناموفق با وقفه کوتاه پیام مناسبی نمایش داده شود. مثلا پیام “سرویس در دسترس نیست لطفا بعدا مجددا تلاش کنید” را نمایش دهیم. زیرا درخواستهای زیاد با وقفه های کوتاه کارایی و سرعت پاسخگویی سیستم را پایین می آورد. ولی در یک عملیات دسته ای یا batch میتوان وقفه میان درخواست ها را در صورت بروز خطا به صورت نمایی بالا برد.
  • اگر پس از ارسال درخواست و چند تلاش مجدد سیستم همچنان خطا می دهد بهتر است که سیستم از ارسال درخواست های بیشتر به آن سرویس جلوگیری کند و خطا را لاگ کرده و آن را گزارش کند. پس از مدتی برای تست سرویس میتواند به صورت آزمایشی درخواستی را ارسال کند. (الگوی قطع جریان که در مقاله ای دیگر بررسی خواهیم کرد)
  • الگوی تلاش دوباره باید روی سرویس هایی صورت گیرد که اجرای دوباره آنها خللی در روند برنامه ایجاد نکند یا به عبارتی idempotent باشد. زیرا ممکن است سرویسی درخواست را گرفته باشد عملیات را اجرا کرده باشد ولی پاسخ به دست درخواست دهنده نرسیده باشد. در اینصورت درخواست مجددا ارسال می شود. بنابراین سرویس باید در برابر اجرای دوباره درخواست امن باشد.
  • خطاها در زمان اجرای سرویس متفاوت اند. بعضی خطاها کاملا اتفاقی هستند و به سرعت برطرف می شوند ولی برخی خطاهای دیگر اندکی زمان نیاز دارند تا برطرف شوند. در زمان پیاده سازی الگوی retry نوع خطا باید مدنظر قرارگیرد. و بر اساس نوع exception وقفه بین درخواست ها و یا تعداد درخواست ها تنظیم شود.
  • در زمان استفاده از این الگو برای بخشی از یک تراکنش باید دو نکته مهم مد نظر قرار گیرد. یک اینکه با استفاده از این الگو شانس موفقیت تراکنش افزایش یابد و دوم اینکه نیاز به undo کردن کل مراحل تراکنش کاهش یابد.
  • باید اطمینان پیدا کنیم که الگوی retry پیاده سازی شده کاملا برای شرایط خطاهای مختلف تست شود. نباید کارایی و سرعت سیستم و همچنین قابلیت اعتماد آن با بارگذاری بیش از حد سرویس ها و منابع و یا بوجود آمدن bottleneck و یا Race Condition کاهش یابد.
  • فقط زمانی از الگوی تلاش دوباره استفاده شود که کل شرایط خطای سرویس درک شده باشد. برای مثال اگر یک سرویس که الگوی retry را پیاده سازی کرده، سرویس دیگری را صدا بزند که آن هم این الگو را پیاده سازی کرده، لایه های متعدد retry سبب افزایش بسیار زیاد وقفه سیستم می شود. در این مواقع بهتر است لایه پایین تر به سرعت خطا دهد و لایه بالاتر خطای لایه پایین تر را مدیریت کند.
  • بسیار مهم است که تمام خطاهای ارتباطی که باعث فراخوانی مجدد سرویس می شود لاگ شود. به این ترتیب خطاهای ارتباطی اپلیکیشن، سرویس ها و سایر منابع شناسایی می شود.
  • باید خطاهایی که به صورت مکرر رخ می دهند، بررسی شوند تا خطاهای موقت و گذرا از خطاهای طولانی مدت تفکیک شوند. در صورتی که خطا گذرا نبود بهتر است لاگ شود و خطا گزارش شود.

چه زمانی از الگوی retry استفاده کنیم

از الگوی retry زمانی استفاده شود که در زمان فراخوانی سرویس یا استفاده از منبعی روی cloud خطای موقتی رخ می دهد و این خطا ها پس از مدت زمان کوتاهی از بین میروند و با ارسال مجدد درخواست احتمال بروز خطا کاهش میابد.

در موارد زیر نباید از این الگو استفاده کنیم:

  • زمانی که خطا موقت نیست و یا زمان طولانی ادامه دارد. در این صورت استفاده از این الگو میتواند منابع سیستم را هدر دهد و سرعت پاسخگویی سیستم را پایین بیاورد.
  • برای خطاهایی که ماهیت بیزینسی دارند. برای مثال خطاهای لایه بیزینس سیستم که مطمئنا موقت نیستند.
  • خطاهایی که به علت مقیاس پذیری پایین سیستم رخ میدهند. اگر یک سرویس به صورت مکرر خطاهای بارگزاری یا شلوغی سرویس میدهد بهتر است، instance های سرویس افزایش پیدا کند.

مثال

در زیر یک مثال ساده به زبان kotlin مشاهد می کنید که تعداد تلاش مجدد و وقفه بین درخواست ها به شکل ساده ای مشخص شده است.


    fun run(action: () -> T): T {
        return try {
            action.invoke()
        } catch (e: Exception) {
            lastFailure = LocalDateTime.now()
            retry(action)
        }

    }

    @Throws(RuntimeException::class)
    private fun retry(action: () -> T): T {
        retryCounter = 1
        while (retryCounter = maxRetries) {
                        break
                    }
                    
                }
            }
        }
        
        throw RuntimeException("Command fails on all retries")
    }

برای زبانهای مختلف کتابخانه های متفاوتی برای پیاده سازی الگوی retry وجود دارد. برای مثال در جاوا کتابخانه  Resilience4j و یا در .Net از کتابخانه  Polly میتوان استفاده کرد.

در فریمورک اسپرینگ در جاوا میتوان از کتابخانه Spring Retry استفاده نمود.


       @Configuration
       @EnableRetry
       class Application {

              @Bean
              fun service(): Service {
                     return Service()
              }

       }

   @Service
   class Service {

    	@Retryable(maxAttempts = 2, include = [RemoteAccessException::class])
    	fun service() {
            // ... do something
            logger.info("Success calling external service")
        }
        
        @Recover
        fun recover(e: RemoteAccessException) {
            // ... do something when call to service fails
            logger.error("Recover for external service")
        }
    }