نحوه‌ی ساخت برنامه‌های انعطاف‌پذیر با Polly

Polly

دراین مقاله میخواهیم راجب نحوه‌ی ساخت برنامه‌های انعطاف‌پذیر با Polly صحبت کنیم. در ابتدا میبینیم که Polly چیست و نحوه ی شروع به کار آن را خواهیم آموخت.

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

این امر صحت دارد که نمی‌توان زمان رخداد استثناء را فهمید، اما می‌توان رفتار برنامه را تحت شرایط نامطلوب، نظیر سناریوی استثناء مدیریت شده یا مدیریت نشده، کنترل کرد.

زمانیکه می گوییم می توان رفتار را در زمانیکه برنامه شکست می‌خورد کنترل کرد، تنها منظورمان گزارش گیری از خطا نیست؛ منظورم این است که این امر مهم است، اما کافی نیست!

امروزه با قدرت cloud computing و تمامی مزایای آن، می توانیم solutionهایی قدرتمند و انعطاف پذیر، با قابلیت دسترسی بالا و مقیاس پذیر بسازیم، اما زیرساخت ابری چالش های خود را نیز به همراه دارد، مانند خطاهای ناپایدار. این امر صحت دارد که خطاهای ناپایدار ممکن است در هر محیط، پلتفرم یا سیستم عاملی رخ دهند، اما این خطاها به دلیل ماهیتشان در فضای ابری محتمل تر هستند، برای نمونه:

  • بسیاری از منابع در یک محیط ابری اشتراکی هستند، بنابراین به منظور حفاظت از آن منابع، دسترسی به آن ها باید کاهش پیدا کند، بدین معنا که با نرخی کنترل شوند، مانند حداکثر گذردهی یا سطح باری مشخص؛ به همین دلیل است که برخی سرویس ها ممکن است در نقطه ای از زمان اتصالات را قبول نکنند.
  • از آنجایی که محیط ابری بطور پویا، بار را در سطح سخت افزار و اجزاء زیرساختی توزیع می کند، و نیز آن ها را بازیابی یا جایگزین می کند، گاهی ممکن است سرویس ها با خطاهای ناپایدار و شکست موقت اتصالات مواجه شوند.
  • و بارزترین دلیل، وضعیت شبکه است، بخصوص هنگامی که ارتباط از اینترنت می گذرد. بنابراین بارهای ترافیکی بسیار سنگین می توانند ارتباط را کند کرده، تأخیر اتصال مازادی را ایجاد کرده، و شکست متناوب اتصالات را موجب شوند.

به منظور دستیابی به انعطاف، برنامه ی شما باید قادر باشد به چالش های زیر پاسخ دهد:

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

در پایان، اگر انعطاف را ضمانت کنیم، قادر خواهیم بود قابلیت اطمینان و دسترسی را بطور ضمنی تضمین کنیم.

قابلیت دسترسی، هنگامی که صحبت از خطای ناپایدار باشد، بدین معناست که منبع همچنان در دسترس است، بنابراین نباید صرفا با یک استثناء پاسخ دهیم.

لذا بخاطرسپاری این چالش ها و مدیریت مناسب آن ها به منظور ساخت بهتر نرم افزار اهمیت دارد.

این جایی است که Polly وارد بازی میشود!

Polly چیست؟

Polly یک کتابخانه ی انعطاف .NET و مدیریت خطای ناپایدار است که توسعه دهندگان را قادر می سازد تا سیاست هایی مانند retry ، circuit breaker ، timeout، bulkhead Isolation ، و fallback را به شیوه ای روان و thread-safe را بیان کنند.

شروع به کار:

هدف ما نشان دادن نحوه ی ساخت استراتژی های منعطف یکپارچه و قدرتمند بر اساس سناریوهای واقعی به شماست .

بنابراین، یک استراتژی منعطف برای اجراهای SQL، و بطور دقیق تر برای پایگاه داده های Azure SQL، خواهیم ساخت.

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

با دنبال کردن الگویی که توضیح خواهیم داد، برای نمونه، می توانید یک استراتژی منعطف برای Azure Service Bus، Redis، اجراهای Elasticsearch و … داشته باشید.

ایده ی ما ساختن استراتژی های ویژه است.

از آنجایی که همگی آن ها خطاهای ناپایدار متفاوت و روش های متفاوتی برای مدیریت آن ها دارند. بیایید شروع کنیم!

انتخاب خطاهای ناپایدار

نخستین چیزی که باید به آن اهمیت داد، آگاهی از اینکه خطاهای ناپایدار برای API یا منبعی که قرار است از آن استفاده کنیم چه هستند، به منظور انتخاب آنهایی که می خواهیم مدیریت کنیم، می باشد.

بطور کلی، می توانیم آن ها را در مستندات رسمی API بیابیم.

در این مورد ما چند خطای ناپایدار بر اساس  مستندات رسمی پایگاه داده های Azure SQL انتخاب خواهیم کرد.

  • ۴۰۶۱۳: پایگاه داده هم اکنون در دسترس نمی باشد.
  • ۴۰۱۹۷: خطا در پردازش درخواست؛ شما به دلیل بروزرسانی در نرم افزار یا سخت افزار ، خرابی سخت افزار یا سایر مشکلات عدم موفقیت سرویس ، این خطا را دریافت می کنید.
  •  ۴۰۵۰۱: سرویس هم اکنون مشغول است.
  • ۴۹۹۱۸: عدم وجود منابع کافی جهت پردازش درخواست.
  • ۴۰۵۴۹: نشست به دلیل داشتن یک تراکنش با زمان اجرای بالا منقضی شده است.
  • ۴۰۵۵۰: نشست به دلیل اشغال چندین منبع در حال استفاده منقضی شده است.

بنابراین در مثال ما، استثنائات SQL فوق را مدیریت خواهیم کرد، اما قطعا می توانید استثنائات را بر اساس نیاز خود مدیریت کنید.

قدرت PolicyWrap

همانطور که پیش تر گفتیم، مبانی Polly را توضیح نخواهیم داد، اما می گوییم که سیاست، عناصر سازنده ی Polly است.

سیاست چیست؟

یک سیاست، حداقل واحد انعطاف پذیری است.

با اشاره به این موضوع، Polly چندین سیاست انعطاف پذیری را ارائه می دهد، از جمله retry، circuit-breaker، timeout، bulkhead isolation، cache و fallback.

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

PolicyWrap شما را قادر می سازد تا سیاست های واحد را به روشی تو در تو، به منظور ساخت یک استراتژی منعطف قدرتمند و یکپارچه، بسته بندی و ترکیب کنید. بنابراین، در رابطه با این سناریو فکر کنید:

هنگامی که یک خطای ناپایدار SQL رخ می دهد، باید حداکثر ۵ مرتبه تلاش مجدد انجام دهید اما، برای هر تلاش، باید بصورت تصاعدی صبر کنید؛

به عنوان مثال ، اولین تلاش ۲ ثانیه صبر می کند ، تلاش دوم ۴ ثانیه صبر می کند و … قبل از اینکه دوباره امتحان کنید

اما شما نمی خواهید منابع خود را برای درخواست های دریافتی جدید هدر دهید ، در حالی که ۳ مرتبه تلاش مجدد انجام داده اید و می دانید که این خطا همچنان ادامه دارد.

در عوض، می خواهید سریع تر شکست خورده و به درخواست های جدید بگویید: “از این کار دست بردار، آزاردهنده است، من به ۳۰ ثانیه استراحت نیاز دارم”.

بدین معنا که پس از سومین تلاش، برای ۳۰ ثانیه ی بعدی، هر درخواست به آن منبع در عوض تلاش برای اجرای عمل، به سرعت شکست خواهد خورد.

همچنین با توجه به اینکه مدت زمانی تصاعدی در هر تلاش صبر میکنیم، در بدترین حالت، که تلاش پنجم است، بیش از ۶۰ ثانیه بعلاوه ی زمانی که خود عمل می گیرد صبر کرده ایم، بنابراین، نمی خواهیم “تا ابد” صبر کنیم؛ در عوض، اینطور بگوییم، ما تمایل داریم تا ۲ دقیقه جهت تلاش برای اجرای یک عمل صبر کنیم، بدین ترتیب، به یک زمان انقضای کلی ۲ دقیقه ای نیاز داریم.

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

بنابراین اگر متوجه شده باشید، جهت دستیابی به یک استراتژی منعطف یکپارچه برای مدیریت آن سناریو، به حداقل ۴ سیاست نیاز خواهیم داشت، از جمله retry، circuit-breaker، timeout، و fallback، اما با کار در قالب یک سیاست واحد در عوض هر کدام بصورت مجزا. بیایید ببینیم جریان سیاست ما به چه صورت خواهد بود تا نحوه ی کار آن را بهتر درک کنیم.

Polly

سیاست های همزمان در مقابل ناهمزمان

پیش از اینکه تعریف سیاست ها را آغاز کنیم، نیاز است زمان و دلیل استفاده از سیاست های همزمان/ناهمزمان و اهمیت عدم ترکیب اجراهای همزمان و ناهمزمان را درک کنیم.

Polly سیاست ها را به همزمان و ناهمزمان تقسیم می کند، نه تنها به این دلیل واضح که جداسازی اجراهای همزمان و ناهمزمان به منظور اجتناب از مشکلات رویکرد ناهمزمان بر همزمان و همزمان بر ناهمزمان صورت می گیرد، اما برای مسائل طراحی به دلیل دام های سیاستی، بدین معنا که سیاست هایی مانند retry، circuit breaker، fallback و …، دام های سیاستی را آشکار می سازند که در آن کاربران می توانند نماینده هایی را جهت فراخوانی شدن هنگام رویدادهای سیاستی خاص الصاق کنند: onRetry، onBreak، onFallback و … .

اما آن نماینده ها وابسته به نوع اجرا هستند، بنابراین، اجراهای همزمان در انتظار دام های سیاستی همزمان، و اجراهای ناهمزمان در انتظار دام های سیاستی ناهمزمان می باشند.

این مسئله ای در مخزن Polly است، جایی که می توانید توضیحاتی عالی در رابطه با اتفاقی که در زمان اجرای یک نماینده ی ناهمزمان از طریق یک سیاست همزمان رخ می دهد، بیابید.

تعریف سیاست ها

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

همچنین از PolicyWrap، که نیاز به دو یا چند سیاست بیشتر جهت بسته بندی و پردازش آن ها به عنوان یک واحد دارد، استفاده خواهیم کرد.

بنابراین، بیایید نگاهی به هر یک از سیاست های واحد بیندازیم.

من تنها ناهمزمان ها را به منظور ساده سازی به شما نشان خواهم داد، اما می توانید پیاده سازی کامل را برای هر دو، همزمان و ناهمزمان، مشاهده کنید.

تفاوت ها در این است که همزمان ها سربار همزمان سیاست و ناهمزمان ها سربار ناهمزمان سیاست را اجرا می کنند.

همچنین، برای دام های سیاستی با سیاست های fallback، جایگزینی همزمان در انتظار نماینده ی ناهمزمان است در حالیکه جایگزینی ناهمزمان در انتظار یک وظیفه می باشد.

انتظار و تلاش مجدد

ما نیاز به سیاستی داریم که برای استثنائات ناپایداری که پیش تر جهت مدیریت انتخاب کردیم، صبر کرده و تلاش مجدد انجام دهد.

بنابراین، به Polly می گوییم SqlExceptions را تنها برای شماره استثنائات بسیار خاص مدیریت کند.

همچنین تعداد مراتبی که باید صبرکند و تأخیر بین هربار تلاش را از طریق یک عقبگرد نمایی بر مبنای تلاش فعلی بیان می کنیم.

public static IAsyncPolicy GetCommonTransientErrorsPolicies(int retryCount) => 
Policy
    .Handle<SqlException>(ex => SqlTransientErrors.Contains(ex.Number))
    .WaitAndRetryAsync(
        // number of retries
        retryCount,
        // exponential back-off
        retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
        // on retry
        (exception, timeSpan, retries, context) =>
            {
            if (retryCount != retries)
                return;
            // only log if the final retry fails
            var msg = $"#Polly #WaitAndRetryAsync Retry {retries}" +
                $"of {context.PolicyKey} " +
                $"due to: {exception}.";
            Log.Error(msg, exception);
    })
    .WithPolicyKey(PolicyKeys.SqlCommonTransientErrorsAsyncPolicy);

circuit breaker

در این سیاست، به Polly می گوییم که پس از تعداد معینی استثناء پیاپی، باید به سرعت شکست خورده و مدار را برای ۳۰ ثانیه باز نگه دارد.

همانطور که می توانید مشاهده کنید، بین روش هایی که استثنائات را مدیریت می کنیم تفاوت وجود دارد؛ در این حالت، یک circuit breaker واحد برای هر استثناء داریم، بر طبق circuit breaker، این سیاست تمام خطاهایی که مدیریت می کنند را به عنوان یک تجمع، و نه بصورت مجزا، می شمارد.

بنابراین تنها می خواهیم مدار را پس از N عمل متوالی اجرا شده بشکنیم زمانیکه این سیاست یک استثناء مدیریت شده، فرضا استثناء DatabaseNotCurrentlyAvailable، داده باشد و نه برای هر یک از استثنائات مدیریت شده توسط این سیاست.

می توانید این موضوع را در مخزن Polly بررسی کنید.

public static IAsyncPolicy[] GetCircuitBreakerPolicies(int exceptionsAllowedBeforeBreaking) 
=> new IAsyncPolicy[]
{
    Policy
        .Handle<SqlException>(ex => ex.Number == (int)SqlHandledExceptions.DatabaseNotCurrentlyAvailable)
        .CircuitBreakerAsync(
            // number of exceptions before breaking circuit
            exceptionsAllowedBeforeBreaking,
            // time circuit opened before retry
            TimeSpan.FromSeconds(30),
            OnBreak,
            OnReset,
            OnHalfOpen)
        .WithPolicyKey($"F1.{PolicyKeys.SqlCircuitBreakerAsyncPolicy}"),

    Policy
        .Handle<SqlException>(ex => ex.Number == (int)SqlHandledExceptions.ErrorProcessingRequest)
        .CircuitBreakerAsync(
            // number of exceptions before breaking circuit
            exceptionsAllowedBeforeBreaking,
            // time circuit opened before retry
            TimeSpan.FromSeconds(30),
            OnBreak,
            OnReset,
            OnHalfOpen)
        .WithPolicyKey($"F2.{PolicyKeys.SqlCircuitBreakerAsyncPolicy}"),
    .
    .
    .
};

timeout

ما از یک استراتژی بدبینانه برای سیاست timeout خود استفاده می کنیم،بدین معنا که نماینده هایی را که هیچ timeout داخلی ندارند را باطل کرده و ابطال را در نظر نمی گیرد.

لذا این استراتژی یک timeout را اجرا کرده، تضمین می کند که همچنان به فراخوان کننده در timeout بازگردد.

public static IAsyncPolicy GetTimeOutPolicy(TimeSpan timeout, string policyName) =>
Policy
    .TimeoutAsync(
        timeout,
        TimeoutStrategy.Pessimistic)
    .WithPolicyKey(policyName);

fallback

همانطور که پیش تر توضیح داده شد، نیاز به یک شانس آخر داریم برای زمانیکه همه چیز اشتباه می شود؛

به همین دلیل است که نه تنها SqlException، بلکه TimeoutRejectedException و BrokenCircuitException را نیز مدیریت می کنیم.

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

public static IAsyncPolicy GetFallbackPolicy<T>(Func<Task<T>> action) =>
Policy
    .Handle<SqlException>(ex => SqlTransientErrors.Contains(ex.Number))
    .Or<TimeoutRejectedException>()
    .Or<BrokenCircuitException>()
    .FallbackAsync(cancellationToken => action(),
        ex =>
        {
            var msg = $"#Polly #FallbackAsync Fallback method used due to: {ex}";
            Log.Error(msg, ex);
            return Task.CompletedTask;
        }
    )
    .WithPolicyKey(PolicyKeys.SqlFallbackAsyncPolicy);

در کنار هم قرار دادن همگی به وسیله ی الگوی سازنده

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

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

بنابراین بیایید نگاهی به مدل سازنده بیندازیم؛ بسیار ساده اما در عین حال بسیار پرکاربرد است.

  
public static IAsyncPolicy GetFallbackPolicy<T>(Func<Task<T>> action) =>
Policy
    .Handle<SqlException>(ex => SqlTransientErrors.Contains(ex.Number))
    .Or<TimeoutRejectedException>()
    .Or<BrokenCircuitException>()
    .FallbackAsync(cancellationToken => action(),
        ex =>
        {
            var msg = $"#Polly #FallbackAsync Fallback method used due to: {ex}";
            Log.Error(msg, ex);
            return Task.CompletedTask;
        }
    )
    .WithPolicyKey(PolicyKeys.SqlFallbackAsyncPolicy);

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

یک SqlPolicyBuilder معمول داریم که سازنده ی دلخواه ما را از طریق متدهای UseAsyncExecutor یا UseSyncExecutor در اختیارمان قرار می دهد.

بنابراین همه ی سازنده ها (SqlAsyncPolicyBuilder و SqlSyncPolicyBuilder) متدهایی را در اختیار قرار می دهند که ما را قادر می سازند یک استراتژی منعطف به شیوه ای انعطاف پذیر بسازیم.

برای نمونه، می توانیم یک استراتژی جهت مدیریت سناریوی پیش تر توضیح داده شده بسازیم، به این صورت:

var builder = new SqlPolicyBuilder();
var resilientAsyncStrategy = builder
    .UseAsyncExecutor()
    .WithFallback(async () => result = await DoFallbackAsync())
    .WithOverallTimeout(TimeSpan.FromMinutes(2))
    .WithTransientErrors(retryCount: 5)
    .WithCircuitBreaker(exceptionsAllowedBeforeBreaking: 3)
    .Build();

result = await resilientAsyncStrategy.ExecuteAsync(async () =>
    {
        return await DoSomethingAsync();
    }
);

در مثال قبل، یک استراتژی ساختیم که کاملا نیازمندی های سناریوی ما را برطرف می کند، و بسیار ساده بود، اینطور نیست؟

بنابراین، از طریق متد UseAsyncExecutor، نمونه ای از ISqlAsyncPolicyBuilder می گیریم.

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

سیاست هایی که باید بسته بندی شوند را دریافت کرده و نماینده را با استفاده از سیاست های داده شده اجرا می کند.

ترتیب سیاست اهمیت دارد

به منظور ساخت یک استراتژی یکپارچه، نیاز است به ترتیبی که سیاست ها را بسته بندی می کنیم، توجه کنیم.

همانطور که در جریان استراتژی منعطف خود متوجه شدید، سیاست fallback بیرونی ترین و circuit breaker داخلی ترین است، چراکه به اولین لینک در زنجیره نیاز داریم تا به تلاش ادامه داده یا به سرعت شکست بخوریم، و آخرین لینک در زنجیره به آرامی تنزل خواهد یافت.

به وضوح، به نیازهای ما بستگی دارد، اما برای مورد ما، آیا بسته بندی circuit breaker با یک timeout جور درمی آید؟

منظور من هنگامیکه می گویم ترتیب سیاست اهمیت دارد این است و به همین دلیل است که سیاست ها را با استفاده از متد WithPolicyKey بصورت الفبایی نامگذاری کردم؛ درون متد Build، سیاست ها را به منظور تضمین استراتژی یکپارچه مرتب می کنم.

هنگامیکه صحبت از بسته بندی سیاست ها می شود، نگاهی به این کاربردهای پیشنهادی بیندازید.

به اشتراک گذاری سیاست ها در بین درخواست ها

ممکن است بخواهیم نمونه ی سیاست ها را به منظور اشتراک وضعیت فعلی آن در بین درخواست ها به اشتراک بگذاریم.

برای نمونه، این امر بسیار مفید خواهد بود زمانیکه مدار، به منظور شکست سریع درخواست ها در عوض هدر دادن منابع در تلاش برای اجرای یک نماینده در مقابل منبعی که هم اکنون در دسترس نیست، باز باشد.

در واقع، این یکی از نیازمندی های سناریوی ماست.

بنابراین SqlPolicyBuilder ما دارای متدهای UseAsyncExecutorWithSharedPolicies و UseSyncExecutorWithSharedPolicies می باشد، که به ما امکان می دهند تا مجدد از نمونه سیاست هایی که از قبل در حال استفاده بودند، استفاده کنیم، به جای اینکه دوباره آن ها را ایجاد کنیم.

این اتفاق در متد Build رخ می دهد و سیاست ها در یک PolicyRegistry ذخیره و در آن بازیابی می شوند.

برای مشاهده ی اینکه کدام سیاست ها وضعیت را در بین درخواست ها به اشتراک می گذارند، نگاهی به این بحث و مستندات رسمی بیندازید.

مثال های کاربرد استراتژی ها با سازنده ی خود

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

اما بیایید چند استراتژی رایج را در اینجا نیز ببینیم.

WithDefaultPolicies

متدی به نام WithDefaultPolicies وجود دارد که ساخت سیاست ها را آسان تر می کند.

یک timeout کلی، انتظار و تلاش مجدد برای خطاهای ناپایدار SQL، و سیاست های circuit breaker را برای آن استثنائات ایجاد می کند؛ اینگونه، می توانید از رایج ترین استراتژی خود به آسانی بهره ببرید.

var builder = new SqlPolicyBuilder();
var resilientAsyncStrategy = builder
    .UseAsyncExecutor()
    .WithDefaultPolicies()
    .Build();

result = await resilientAsyncStrategy.ExecuteAsync(async () =>
{
    return await DoSomethingAsync();
});

// the analog strategy will be:
resilientAsyncStrategy = builder
    .UseAsyncExecutor()
    .WithOverallTimeout(TimeSpan.FromMinutes(2))
    .WithTransientErrors(retryCount: 5)
    .WithCircuitBreaker(exceptionsAllowedBeforeBreaking: 3)
    .Build();

WithTimeoutPerRetry

این متد امکان معرفی یک timeout به ازای هر تلاش مجدد را، به منظور مدیریت timeout برای هر تلاش علاوه بر یک timeout کلی، در اختیار ما قرار می دهد.

بنابراین در مثال بعدی، اگر تلاش بیش از ۳۰۰ میلی ثانیه زمان بگیرد، یک TimeoutRejectedException خواهد داد.

var builder = new SqlPolicyBuilder();
var resilientAsyncStrategy = builder
    .UseAsyncExecutor()
    .WithDefaultPolicies()
    .WithTimeoutPerRetry(TimeSpan.FromMilliseconds(300))
    .Build();

result = await resilientAsyncStrategy.ExecuteAsync(async () =>
{
    return await DoSomethingAsync();
});

WithTransaction

این متد ما را قادر می سازد تا در زمانیکه نماینده تحت یک تراکنش اجرا می شود، خطاهای ناپایدار SQL مربوط به تراکنش ها را مدیریت کنیم.

var builder = new SqlPolicyBuilder();
var resilientAsyncStrategy = builder
    .UseAsyncExecutor()
    .WithDefaultPolicies()
    .WithTransaction()
    .Build();

result = await resilientAsyncStrategy.ExecuteAsync(async () =>
{
    return await DoSomethingAsync();
});

To have in mind

از بسته بندی چندین عملیات یا منطق درون اجراکننده ها خودداری کنید، بخصوص زمانیکه خودتوان نیستند، اگرنه ممکن است اوضاع بهم بریزد. در مورد این سناریو فکر کنید:

var builder = new SqlPolicyBuilder();
var resilientAsyncStrategy = builder
    .UseAsyncExecutor()
    .WithDefaultPolicies()
    .Build();

await resilientAsyncStrategy.ExecuteAsync(async () =>
{
    await CreateSomethingAsync();
    await UpdateSomethingAsync();
    await DeleteSomethingAsync();
});

در سناریوی قبلی، اگر چیزی اشتباه میشد، فرضا درون عملیات های UpdateSomethingAsync یا DeleteSomethingAsync، تلاش مجدد بعدی سعی خواهد کرد متدهای CreateSomethingAsync یا UpdateSomethingAsync را مجدد اجرا کند، که این کار می تواند اوضاع را بهم بریزد؛

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

همچنین، می توانید آن سناریو را به این شکل مدیریت کنید:

var builder = new SqlPolicyBuilder();
var resilientAsyncStrategy = builder
    .UseAsyncExecutor()
    .WithDefaultPolicies()
    .Build();

await resilientAsyncStrategy.ExecuteAsync(async () =>
{
    await CreateSomethingAsync();
});

await resilientAsyncStrategy.ExecuteAsync(async () =>
{
    await UpdateSomethingAsync();
});

await resilientAsyncStrategy.ExecuteAsync(async () =>
{
    await DeleteSomethingAsync();
});

Wrapping up

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

لذا شما را به ایجاد سازنده ها به منظور اختصاصی کردن سیاست های خود تشویق می کنم؛

همانطور که پیش تر گفتیم، می توانید الگوها/پیشنهادات را جهت ساخت سازنده های خود، فرضا برای Redis، Azure Service Bus، Elasticsearch، HTTP و …، دنبال کنید.

نکته ی کلیدی، توجه به این موضوع است که اگر می خواهیم برنامه هایی منعطف بسازیم، نمی توانیم با هر خطایی درست مانند یک Exception برخورد کنیم؛ هر منبع در هر سناریو، استثنائات خود و روشی مناسب جهت مدیریت آن ها دارد.

  • پسورد: www.mspsoft.com
زهره سلطانیان

نوشته‌های مرتبط

دیدگاه‌ها

*
*

این سایت از اکیسمت برای کاهش هرزنامه استفاده می کند. بیاموزید که چگونه اطلاعات دیدگاه های شما پردازش می‌شوند.