عنوان:

‫استفاده از Polly برای مدیریت خطاهای موقتی در ASP.NET Core


نویسنده: محمد شریفی
تاریخ: ۱۴۰۳/۱۰/۲۰ ۱۹:۵۵
آدرس: www.dntips.ir
Polly چیست؟
Polly یک کتابخانه‌ی محبوب برای مدیریت خطاهای موقتی و تحمل خطا در دات‌نت است که به توسعه‌دهندگان این امکان را می‌دهد تا استراتژی‌هایی مانند retry، circuit breaker، timeout و fallback را در برنامه‌های خود پیاده‌سازی کنند. در این مقالە، نحوه‌ی استفاده از Polly ورژن 7 در یک برنامه‌ی ASP.NET Core برای مدیریت خطاهای موقتی و بهبود قابلیت اطمینان سیستم، توضیح خواهیم داد.

چرا باید Polly را در ASP.NET Core استفاده کنیم؟
در برنامه‌های مدرن، به ویژه در میکروسرویس‌ها و سیستم‌های توزیع‌شده، خطاهای موقتی شایع هستند. این خطاها می‌توانند به دلیل مشکلات شبکه، خرابی‌های API خارجی، تایم‌اوت‌های پایگاه داده و سایر مشکلات غیرقابل پیش‌بینی رخ دهند. Polly کمک می‌کند تا این خطاها را مدیریت کرده و برنامه را مقاوم‌تر کنید.

مزایای استفاده از Polly شامل موارد زیر است:
  • افزایش قابلیت اطمینان برنامه: با تلاش مجدد خودکار برای درخواست‌های شکست‌خورده یا قطع مدار در صورت شکست‌های مداوم، از بروز خرابی‌های بیشتر جلوگیری می‌شود.
  • بهبود تجربه کاربری: Polly می‌تواند پاسخ‌های بازگشتی (مثل داده‌های پیش‌فرض یا نتایج کش‌شده) هنگام بروز خطا ارائه دهد.
  • مقاومت در برابر خطا در سیستم‌های توزیع‌شده: Polly کمک می‌کند تا درخواست‌ها به خدمات خارجی یا پایگاه داده‌ها با مدیریت خطا انجام شود.

چگونه Polly کار می‌کند: مفاهیم اصلی
Polly از سیاست‌ها برای محافظت از عملیات شما در برابر خطاها استفاده می‌کند. برخی از رایج‌ترین سیاست‌ها عبارتند از:

  • Retry Policy: تلاش می‌کند تا یک عملیات شکست‌خورده را چندین بار با یک فاصله مشخص بین هر تلاش، دوباره اجرا کند.
  • Circuit Breaker Policy: پس از تعداد مشخصی از شکست‌ها، مدار قطع می‌شود و عملیات به طور موقت متوقف می‌شود.
  • Timeout Policy: اگر یک عملیات بیش از مدت زمان مشخصی طول بکشد، آن را متوقف می‌کند.
  • Fallback Policy: وقتی عملیات شکست می‌خورد، یک اقدام یا پاسخ جایگزین را ارائه می‌دهد.
  • Bulkhead Isolation: تعداد عملیات همزمانی را که به یک منبع خاص دسترسی پیدا می‌کنند، محدود می‌کند تا از بار اضافی جلوگیری شود.

نصب Polly در ASP.NET Core
برای شروع استفاده از Polly در یک پروژه ASP.NET Core، باید پکیج Polly را از طریق NuGet نصب کنید. این کار را می‌توانید از طریق مدیر پکیج NuGet یا CLI دات‌نت انجام دهید.

نصب از طریق CLI دات‌نت:
dotnet add package Polly

Retry Policy
یک Retry Policy به شما این امکان را می‌دهد که یک عملیات شکست‌خورده را چندین بار با یک فاصله مشخص بین هر تلاش، دوباره اجرا کنید.

مثال: پیاده‌سازی یک Retry Policy با Polly:
services.AddHttpClient("MyHttpClient")
    .AddPolicyHandler(Policy.Handle<HttpRequestException>()
        .WaitAndRetryAsync(3, attempt => TimeSpan.FromSeconds(2)));  // Retry 3 times with a 2 second delay

  • <Handle<T: نوع استثناهایی را که باید مدیریت شوند مشخص می‌کند. می‌توانید چندین نوع استثنا را با زنجیره‌سازی <Handle<T مدیریت کنید.
  • WaitAndRetryAsync: سیاست Retry را پیکربندی می‌کند، که در این مثال 3 بار تلاش مجدد انجام می‌دهد و هر بار با فاصله 2 ثانیه بین تلاش‌ها.

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

مثال: پیاده‌سازی Circuit Breaker Policy:
services.AddHttpClient("MyHttpClient")
    .AddPolicyHandler(Policy.Handle<HttpRequestException>()
        .CircuitBreakerAsync(3, TimeSpan.FromSeconds(30)));  // Circuit breaker after 3 failures for 30 seconds

این اطمینان می‌دهد که پس از 3 شکست، تلاش‌های بعدی برای 30 ثانیه متوقف می‌شوند.

Timeout Policy
یک Timeout Policy مدت زمان مشخصی را برای یک عملیات تعیین می‌کند. اگر عملیات بیشتر از آن زمان طول بکشد، متوقف می‌شود.

مثال: پیاده‌سازی Timeout Policy:
services.AddHttpClient("MyHttpClient")
    .AddPolicyHandler(Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(10)));  // Timeout after 10 seconds


Fallback Policy
یک Fallback Policy هنگامی که یک عملیات شکست می‌خورد، یک اقدام یا پاسخ جایگزین را ارائه می‌دهد.

مثال: پیاده‌سازی Fallback Policy:
services.AddHttpClient("MyHttpClient")
    .AddPolicyHandler(Policy.Handle<HttpRequestException>()
        .FallbackAsync(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("Fallback data") }));

اگر یک درخواست HTTP شکست خورد، این سیاست یک پاسخ بازگشتی با پیغام "Fallback data" باز می‌گرداند.

استفاده از Polly با HttpClientFactory
در ASP.NET Core، پیشنهاد می‌شود از IHttpClientFactory برای مدیریت نمونه‌های HttpClient استفاده کنید. HttpClientFactory این امکان را می‌دهد که سیاست‌های Polly را برای درخواست‌های HttpClient پیکربندی کنید.

مثال:
public void ConfigureServices(IServiceCollection services)
{
    services.AddHttpClient("MyHttpClient")
        .AddTransientHttpErrorPolicy(policyBuilder => 
            policyBuilder.WaitAndRetryAsync(3, _ => TimeSpan.FromSeconds(2))) // Retry 3 times with a 2 second delay
        .AddTransientHttpErrorPolicy(policyBuilder => 
            policyBuilder.CircuitBreakerAsync(3, TimeSpan.FromSeconds(30))); // Circuit breaker after 3 failures for 30 seconds
}

IHttpClientFactory این امکان را به شما می‌دهد که پیکربندی HttpClient و سیاست‌های Polly را به صورت متمرکز انجام دهید.


الگوهای پیشرفته Polly
1) استفاده از الگوی WrapAsync برای ترکیب سیاست‌ها
می‌توانید چندین سیاست را با هم ترکیب کنید تا از سناریوهای مختلف خطا جلوگیری کنید. Polly این امکان را به شما می‌دهد تا سیاست‌ها را با استفاده از متد AddPolicyHandler زنجیره‌ای کنید.

مثال: ترکیب سیاست‌های Retry، Circuit Breaker و Timeout:
services.AddHttpClient("MyHttpClient")
    .AddPolicyHandler(Policy.WrapAsync(
        Policy.Handle<HttpRequestException>()
            .WaitAndRetryAsync(3, _ => TimeSpan.FromSeconds(2)),   // Retry Policy
        Policy.Handle<HttpRequestException>()
            .CircuitBreakerAsync(3, TimeSpan.FromSeconds(30)),   // Circuit Breaker Policy
        Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(10)))); // Timeout Policy

با این کار، شما می‌توانید سیاست‌های retry، circuit breaker و timeout را به طور همزمان پیاده‌سازی کنید.

2) Bulkhead Isolation
Bulkhead Isolation به شما این امکان را می‌دهد که تعداد عملیات همزمانی که به یک منبع خاص دسترسی پیدا می‌کنند را محدود کنید تا از بار اضافی جلوگیری شود.

مثال:
services.AddHttpClient("MyHttpClient")
    .AddPolicyHandler(Policy.BulkheadAsync<HttpResponseMessage>(maxParallelRequests: 10));

این سیاست محدود می‌کند که فقط 10 درخواست همزمان به یک سرویس خارجی ارسال شود.

3) استفاده از IHttpClientFactory به‌جای ایجاد مستقیم HttpClient
یکی از بهترین شیوه‌ها برای استفاده از Polly در ASP.NET Core این است که همیشه از IHttpClientFactory استفاده کنید. HttpClient باید برای عملکرد بهتر، استفاده مجدد و جلوگیری از مشکلات مربوط به DNS resolution و TCP connection exhaustion (که در صورت استفاده مستقیم از HttpClient بدون مدیریت مناسب رخ می‌دهد) از طریق این اینترفیس مدیریت شود.
public void ConfigureServices(IServiceCollection services)
{
    services.AddHttpClient("MyHttpClient")
        .AddTransientHttpErrorPolicy(policy => policy.WaitAndRetryAsync(3, _ => TimeSpan.FromSeconds(2)))
        .AddTransientHttpErrorPolicy(policy => policy.CircuitBreakerAsync(3, TimeSpan.FromSeconds(30)));
}

4) سیاست‌های پیوسته (Transient Fault Handling)
Polly معمولاً برای خطاهای موقتی (Transient Faults) طراحی شده است. برای مثال، خطاهایی که ناشی از مشکلات شبکه یا خرابی‌های API خارجی هستند. در صورتیکه خطای دریافتی ناشی از یک مشکل دائمی باشد (مثل 404 یا 500)، معمولاً توصیه نمی‌شود که از Polly برای retry استفاده شود.
برای مثال، TransientFaultHandling را می‌توان به این صورت مدیریت کرد:
services.AddHttpClient("MyHttpClient")
    .AddPolicyHandler(Policy.Handle<HttpRequestException>()
        .OrResult<HttpResponseMessage>(r => r.StatusCode == HttpStatusCode.RequestTimeout)
        .WaitAndRetryAsync(3, _ => TimeSpan.FromSeconds(5)));

5) استفاده از ILogger برای ثبت گزارش‌ها
یکی از بهترین شیوه‌ها برای ردیابی عملکرد و مشکلات خطای Polly این است که از ILogger برای ثبت گزارش‌ها استفاده کنید. این کار به شما کمک می‌کند که با کمک ابزارهای نظارتی و گزارش‌دهی، عملکرد سیستم را در زمان واقعی پیگیری کنید.
مثال:
services.AddHttpClient("MyHttpClient")
    .AddPolicyHandler(Policy.Handle<HttpRequestException>()
        .WaitAndRetryAsync(3, attempt => 
        {
            _logger.LogWarning("Retry attempt #{Attempt}", attempt);
            return TimeSpan.FromSeconds(2);
        }));

6) سیاست‌های زمان‌بندی (Retry with Exponential Backoff)
یکی از روش‌های بسیار موثر برای مدیریت خطاهای موقتی، استفاده از Exponential Backoff برای سیاست‌های retry است. این الگو زمانیکه سیستم در حال شکست است، تاخیر بین تلاش‌ها را افزایش می‌دهد تا فشار اضافی بر سیستم وارد نشود.
مثال:
services.AddHttpClient("MyHttpClient")
    .AddPolicyHandler(Policy.Handle<HttpRequestException>()
        .WaitAndRetryAsync(5, attempt => 
            TimeSpan.FromSeconds(Math.Pow(2, attempt))));  // Exponential backoff
در اینجا، برای هر تلاش، زمان انتظار به صورت نمایی افزایش می‌یابد (مثلاً: 1s, 2s, 4s, 8s, 16s).

7) استفاده از سیاست‌های فیلتر (Filtering Policies)
Polly به شما این امکان را می‌دهد که سیاست‌های خود را برای نوع خاصی از استثناءها یا نتایج، فیلتر کنید. این کار می‌تواند به شما کمک کند که فقط روی شرایط خاص تمرکز کنید و باقی شرایط را نادیده بگیرید.
برای مثال، می‌توانید فقط خطاهای خاص (مثل HttpRequestException یا کدهای وضعیت خاص HTTP) را مدیریت کنید:
services.AddHttpClient("MyHttpClient")
    .AddPolicyHandler(Policy.Handle<HttpRequestException>()
        .OrResult<HttpResponseMessage>(r => r.StatusCode == HttpStatusCode.RequestTimeout)
        .WaitAndRetryAsync(5, _ => TimeSpan.FromSeconds(2)));
این کار به شما این امکان را می‌دهد که سیاست‌ها را فقط برای شرایط خاص به‌کار بگیرید.

8) کش (Caching)
یکی از قابلیت‌های مهم Polly، پشتیبانی از کش کردن نتایج است. این قابلیت به شما این امکان را می‌دهد که پاسخ‌ها را برای درخواست‌های تکراری در بازه زمانی مشخص ذخیره کنید. این کار می‌تواند بار اضافی روی سرورهای خارجی را کاهش داده و عملکرد برنامه را بهبود ببخشد.
مثال:
var cacheProvider = new MemoryCacheProvider(new MemoryCache(new MemoryCacheOptions()));
var cachePolicy = Policy.CacheAsync<HttpResponseMessage>(
    cacheProvider.AsyncFor<HttpResponseMessage>(),
    TimeSpan.FromMinutes(5));

services.AddHttpClient("MyHttpClient")
    .AddPolicyHandler(cachePolicy);

در اینجا، از یک MemoryCache به عنوان حافظه پنهان استفاده شده است و پاسخ‌ها به مدت 5 دقیقه در کش ذخیره می‌شوند.

9) Rate Limit
Rate Limiting به شما این امکان را می‌دهد که تعداد درخواست‌هایی که در یک بازه زمانی ارسال می‌شود را محدود کنید. این کار از سرریز کردن منابع و سرویس‌های خارجی جلوگیری می‌کند و با محدودیت‌های API خارجی سازگار است.
مثال:
var rateLimitPolicy = Policy.RateLimitAsync(10, TimeSpan.FromSeconds(1)); 

services.AddHttpClient("MyHttpClient")
    .AddPolicyHandler(rateLimitPolicy);

این سیاست اجازه می‌دهد حداکثر 10 درخواست در هر ثانیه به یک سرویس خارجی ارسال شود.

در ادامه، یک کلاینت API پایدار و مقاوم با استفاده از کتابخانه Polly طراحی می کنیم که با ترکیب سیاست‌های Retry (تلاش مجدد)، Timeout (زمان‌بندی) و Circuit Breaker (مدیریت شکست مدار)، به مدیریت خطاهای موقتی و افزایش پایداری سیستم کمک می‌کند. این رویکرد تضمین می‌کند که کلاینت در برابر خطاهای شبکه‌ای و سایر مشکلات غیرمنتظره مقاوم باقی بماند.
public class MyApiService
{
    private readonly HttpClient _httpClient;
    private readonly ILogger<MyApiService> _logger;

    public MyApiService(IHttpClientFactory httpClientFactory, ILogger<MyApiService> logger)
    {
        _httpClient = httpClientFactory.CreateClient("MyHttpClient");
        _logger = logger;
    }

    public async Task<string> GetApiDataAsync()
    {
        try
        {
            var response = await _httpClient.GetAsync("https://api.example.com/data");

            response.EnsureSuccessStatusCode();

            return await response.Content.ReadAsStringAsync();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error occurred while calling the API.");
            throw;
        }
    }
}


نتیجه‌گیری
Polly یکی از ابزارهای مهم برای افزودن قابلیت‌های تحمل خطا به برنامه‌های ASP.NET Core است. با استفاده از Retry Policies، Circuit Breaker Policies، Timeout Policies و سایر سیاست‌ها، می‌توانید خطاهای موقتی و شبکه‌ای را مدیریت کرده و سیستم‌های مقاوم‌تری بسازید. همچنین بهترین شیوه‌ها مانند استفاده از IHttpClientFactory، ثبت گزارش‌ها، استفاده از Exponential Backoff و نظارت بر وضعیت می‌تواند به بهبود عملکرد و قابلیت اطمینان برنامه‌های شما کمک کند.

نظرات

  • محمد شریفی در ۱۴۰۳/۱۰/۲۳ ۰۹:۴۷
    تغییرات کلیدی Polly در نسخه 8:

    1) استراتژی‌ها به جای سیاست‌ها
    در نسخه 8، اصطلاح "Policy" به "Strategy" تغییر یافته است. این تغییر نام باعث بهبود طراحی API و ادغام بهتر آن با الگوهای شی‌گرا می‌شود. هر مکانیزم افزونگی (مانند Retry، Timeout و غیره) اکنون به‌صورت یک "Strategy" پیاده‌سازی شده است.

    2) خطوط لوله انعطاف پذیر (Resilience Pipelines)
    این ویژگی مشابه Policy Wrap در نسخه‌های قبلی است، اما اجازه می‌دهد که چندین استراتژی به‌طور مؤثرتری ترکیب شوند. این ویژگی انعطاف‌پذیری بیشتری فراهم می‌کند تا بتوانید استراتژی‌های مختلف را به‌طور همزمان اعمال کنید.

    3) پشتیبانی بهتر از عملیات‌های غیرهمزمان
    نسخه 8 به‌طور طبیعی از عملیات‌های غیرهمزمان پشتیبانی می‌کند. دیگر نیازی به بسته‌بندی Task یا ValueTask برای عملیات‌های async نیست. می‌توانید مستقیماً از متدهای ExecuteAsync و RetryAsync برای عملیات‌های غیرهمزمان استفاده کنید.

    4) حذف API‌های استاتیک
    در نسخه 8، تمام API‌ها به‌صورت شی‌گرا طراحی شده‌اند. این تغییر به بهبود تست‌پذیری و توسعه‌پذیری Polly کمک می‌کند. این به این معنی است که به جای استفاده از متدهای استاتیک که در نسخه‌های قبلی رایج بود، شما باید از شی‌ها (objects) و متدهای آن‌ها برای استفاده از استراتژی‌ها استفاده کنید.

    5) پشتیبانی داخلی از نظارت و ثبت داده‌ها (Telemetry)
    در نسخه 8، Polly به‌طور پیش‌فرض ابزاری برای نظارت و ثبت داده‌ها (Telemetry) دارد که شما می‌توانید برای تجزیه و تحلیل و بهبود عملکرد برنامه‌تان از آن استفاده کنید. این ابزار می‌تواند به شما کمک کند تا به‌راحتی مشکلات عملکردی را شناسایی کنید و میزان موفقیت و شکست استراتژی‌های مختلف را بررسی کنید.

    6) بهبود عملکرد و تخصیص منابع
    نسخه 8 به‌ویژه در تخصیص حافظه و مصرف منابع بهبود‌های زیادی داشته است. این باعث می‌شود که Polly برای محیط‌های با منابع محدود، مانند برنامه‌های مبتنی بر ابر، مناسب‌تر باشد.

    تفاوت‌های کلیدی با نسخه 7
    در نسخه 7، از کلاس Policy برای تعریف استراتژی‌ها استفاده می‌شد که عمدتاً بر پایه متدهای استاتیک بود.
    در نسخه 8:
    • از Resilience Pipelines برای ترکیب چندین استراتژی استفاده می‌شود.
    • API به مدل شی‌گرا تغییر یافته که پشتیبانی بهتری از عملیات‌های غیرهمزمان فراهم می‌کند.
    • تغییرات در API باعث انعطاف‌پذیری بیشتر و ادغام بهتر استراتژی‌ها شده است.

    نحوه استفاده از Polly نسخه 8
    در اینجا یک مثال ساده از استفاده از Polly نسخه 8 آورده شده است. در این مثال، ما درخواست HTTP را با استفاده از استراتژی‌های Retry و Timeout به‌طور همزمان مدیریت می‌کنیم.

    گام 1: نصب Polly
    ابتدا Polly را از NuGet نصب کنید:
    dotnet add package Polly.Core

    گام 2: کد نمونه برای Polly نسخه 8
    در این کد، از یک Resilience Pipeline برای مدیریت استراتژی‌های Retry و Timeout استفاده می‌شود.
    using Polly;
    using System;
    using System.Net.Http;
    using System.Threading.Tasks;
    
    public class PollyExample
    {
        public static async Task Main(string[] args)
        {
            // Define the Retry strategy
            var retryStrategy = new ResilienceStrategyBuilder()
                .AddRetry(3)
                .Build();
    
            // Define the Timeout strategy
            var timeoutStrategy = new ResilienceStrategyBuilder()
                .AddTimeout(TimeSpan.FromSeconds(5))
                .Build();
    
            // Combine the strategies into a resilience pipeline
            var resiliencePipeline = new ResiliencePipelineBuilder()
                .AddStrategy(retryStrategy)
                .AddStrategy(timeoutStrategy)
                .Build();
    
            // Execute the HTTP request using the resilience pipeline
            var response = await resiliencePipeline.ExecuteAsync(async () =>
            {
                var httpClient = new HttpClient();
                return await httpClient.GetAsync("http://example.com");
            });
    
            Console.WriteLine($"Response Status Code: {response.StatusCode}");
        }
    }

    توضیح کد:
    1. تعریف استراتژی Retry:
    var retryStrategy = new ResilienceStrategyBuilder()
        .AddRetry(3)
        .Build();

    در این بخش، یک استراتژی Retry (تلاش مجدد) ساخته می‌شود که به کد اجازه می‌دهد در صورت بروز خطا، تا ۳ بار تلاش مجدد انجام دهد. AddRetry(3) به این معناست که درخواست در صورت شکست، تا ۳ بار دوباره ارسال می‌شود.

    2. تعریف استراتژی Timeout:
    var timeoutStrategy = new ResilienceStrategyBuilder()
        .AddTimeout(TimeSpan.FromSeconds(5))
        .Build();

    در اینجا یک استراتژی Timeout (مهلت زمانی) تعریف می‌شود که اگر درخواست HTTP بیشتر از ۵ ثانیه طول بکشد، به عنوان یک خطا در نظر گرفته شده و درخواست لغو می‌شود.

    3. ترکیب استراتژی‌ها در یک Resilience Pipeline:
    var resiliencePipeline = new ResiliencePipelineBuilder()
        .AddStrategy(retryStrategy)
        .AddStrategy(timeoutStrategy)
        .Build();

    در این مرحله، استراتژی‌های Retry و Timeout در یک Pipeline ترکیب می‌شوند. این Pipeline به صورت یک توالی از استراتژی‌ها عمل می‌کند، به این معنا که ابتدا استراتژی Retry بررسی می‌شود و اگر درخواست موفق نشد، تا ۳ بار دوباره تلاش می‌کند. سپس، اگر درخواست همچنان موفق نشد و به دلیل طولانی شدن بیش از ۵ ثانیه مواجه شد، استراتژی Timeout وارد عمل می‌شود.

    4. اجرای درخواست HTTP با استفاده از Resilience Pipeline
    var response = await resiliencePipeline.ExecuteAsync(async () =>
    {
        var httpClient = new HttpClient();
        return await httpClient.GetAsync("http://example.com");
    });
    در این بخش، درخواست HTTP به آدرس http://example.com ارسال می‌شود. این درخواست درون Pipeline اجرا می‌شود، که به این معنی است که استراتژی‌های Retry و Timeout به صورت خودکار مدیریت می‌شوند. اگر درخواست با موفقیت انجام شود، نتیجه آن در response ذخیره می‌شود.

    مهاجرت از نسخه 7 به 8
    برای ارتقاء به نسخه 8، ابتدا Polly را به آخرین نسخه در پروژه خود به‌روزرسانی کنید:
    dotnet add package Polly --version 8.x.x