بررسی قابلیت بازآزمایی توکار (Configurable Retry Logic) در Microsoft.Data.SqlClient
نویسنده: وحید نصیری
تاریخ: ۱۴۰۵/۰۶/۱۴ ۱۰:۳۰
آدرس: www.dntips.ir
چکیده: خطاهای گذرا (Transient Faults)، نظیر قطع ناگهانی ارتباط، تاخیرهای موقت شبکه و تغییر وضعیت سرور در محیطهای ابری، از اجتنابناپذیرترین چالشهای معماری مدرن نرمافزار هستند. سالها توسعهدهندگان پلتفرم داتنت (.NET) برای مقابله با این خطاها به پکیجهای ثالث نظیر Polly متکی بودند. با معرفی فضای نامMicrosoft.Data.SqlClientو اضافه شدن قابلیت بازآزمایی پیکربندیپذیر (Configurable Retry Logic)، این مسئولیت به طور توکار و در لایه درایور پایگاهداده تعبیه شدهاست. این مقاله به بررسی چرایی مهاجرت از درایور سنتیSystem.Data.SqlClientبهMicrosoft.Data.SqlClient، نحوه پیادهسازی مکانیزمهای بازآزمایی برایSqlConnectionوSqlCommand، تفاوتهای کلیدی آن با Polly، و ملاحظات حساس معماری پیرامون تراکنشها و پایستگی (Idempotency) میپردازد.
System.Data.SqlClient را معرفی کرد. برای بیش از یک دهه و نیم، این فضای نام ستون فقرات ارتباطات دادهای میلیونها سیستم سازمانی (Line-of-Business) بود. با گذشت زمان و گسترش رایانش ابری (مانند Azure SQL)، این پکیج به دلیل وابستگی مستقیم به چارچوب Base Class Library (BCL) دچار کندی در بهروزرسانی و «پوسیدگی کد» (Code Calcification) شد.Microsoft.Data.SqlClient تغییر داد. این درایور مستقل از سیستمعامل، از طریق بستههای NuGet عرضه شده و چرخههای انتشار سریعتری دارد. اگرچه این درایور جایگزین مستقیم (Drop-in Replacement) برای System.Data.SqlClient است و سازگاری عقروعطف (Backward Compatibility) بالایی دارد، اما آمارها نشان میدهد بخش قابلتوجهی از پروژههای فعال هنوز این مهاجرت ضروری را انجام ندادهاند.SqlConnection)SqlConfigurableRetryFactory و گزینههای SqlRetryLogicOption استفاده میشود:using System;
using System.Threading.Tasks;
using Microsoft.Data.SqlClient;
public class DatabaseConnectionFactory
{
private readonly string _connectionString;
private readonly SqlRetryLogicBaseProvider _retryProvider;
public DatabaseConnectionFactory(string connectionString)
{
_connectionString = connectionString;
// تعریف پارامترهای منطق بازآزمایی
var retryOptions = new SqlRetryLogicOption
{
NumberOfTries = 5, // یک بار تلاش اولیه + ۴ بار بازآزمایی
DeltaTime = TimeSpan.FromSeconds(1), // وقفه پایه برای پسروی نمایی
MaxTimeInterval = TimeSpan.FromSeconds(20), // حداکثر سقف مجاز برای هر وقفه
TransientErrors = null // null به معنای استفاده از لیست خطاهای گذرای پیشفرض درایور است
};
// ایجاد ارائهدهنده بازآزمایی نمایی با جیتر (Jitter) داخلی
_retryProvider = SqlConfigurableRetryFactory.CreateExponentialRetryProvider(retryOptions);
}
public async Task<SqlConnection> CreateOpenConnectionAsync()
{
var connection = new SqlConnection(_connectionString)
{
RetryLogicProvider = _retryProvider
};
// در صورت بروز خطای گذرا، به صورت خودکار بازآزمایی انجام میشود
await connection.OpenAsync();
return connection;
}
}نکته:MaxTimeInterval سقف هر وقفه منفرد را مشخص میکند، نه کل زمان سپریشده در تمامی دفعات تلاش.SqlCommand) و شرط توانمندی مجدد (Idempotency)AuthorizedSqlCondition امکان فیلتر کردن دستورات ایمن را فراهم میکند:var commandRetryOptions = new SqlRetryLogicOption
{
NumberOfTries = 3,
DeltaTime = TimeSpan.FromMilliseconds(500),
MaxTimeInterval = TimeSpan.FromSeconds(5),
// اعمال بازآزمایی صرفاً روی کوئریهای فاقد اثر جانبی (Idempotent)
AuthorizedSqlCondition = cmd =>
cmd is SqlCommand sqlCmd &&
sqlCmd.CommandText.TrimStart().StartsWith("SELECT", StringComparison.OrdinalIgnoreCase)
};
var commandRetryProvider = SqlConfigurableRetryFactory.CreateExponentialRetryProvider(commandRetryOptions);
using var command = new SqlCommand("SELECT * FROM Products WHERE CategoryId = @id", connection)
{
RetryLogicProvider = commandRetryProvider
};
command.Parameters.AddWithValue("@id", 10);| معیار | کتابخانه Polly | درایور Microsoft.Data.SqlClient |
| دامنه پوشش (Scope) | سراسر لایههای اپلیکیشن (HTTP، صفها، کش و ...) | صرفاً لایه پایگاهداده و ارتباطات SQL Server |
| آگاهی از بافتار SQL (SQL-Aware) | نیازمند تنظیم دستی کد خطاها یا فیلترهای استثنا | درک بومی خطاهای گذرا و ساختار پکتهای TDS |
| مدیریت تراکنشها | بیاطلاع از وضعیت تراکنشهای باز در سطح سوکت | شناسایی فعال تراکنشها و جلوگیری از بازآزمایی مخرب |
| سادگی و سربار | نیازمند تعریف Pipeline و الگوهای Decorator/Wrapper | انتساب مستقیم یک Provider قابل استفاده مجدد (Singleton) |
| الگوهای پیشرفته | Circuit Breaker, Fallback, Hedging, Rate Limiter | صرفاً راهبردهای Retry (Fixed, Incremental, Exponential) |
SqlTransaction) شکست بخورد، وضعیت تراکنش در سرور نامشخص (Orphaned / Uncommitted) میشود. به همین دلیل، درایور نیتیو به صورت پیشفرض دستوراتی را که درون یک تراکنش فعال اجرا میشوند بازآزمایی نمیکند.راه حل: در فرآیندهای تراکنشی، باید کل بلوک تراکنش (BeginTransactionتاCommit) از نقطه شروع مجدداً تکرار شود، نه اینکه دستورِ شکستخورده به تنهایی درون تراکنش معیوب Retry گردد.
ExecutionStrategy (مانند EnableRetryOnFailure) است:options.UseSqlServer(connectionString, sqlOptions =>
{
sqlOptions.EnableRetryOnFailure(
maxRetryCount: 5,
maxRetryDelay: TimeSpan.FromSeconds(10),
errorNumbersToAdd: null);
});SqlClient میتواند چرخه تو در تو تولید کند (مثلاً ۵ بار بازآزمایی در لایه درایور به ازای هر ۱ بار بازآزمایی لایه ORM که منجر به ۲۵ بار تلاش میشود). تعیین مسئولیت باید در یک لایه متمرکز باشد.BaselineTransientErrors در نسخههای جدید، میتوان کدهای اختصاصی کسبوکار یا خطاهای خاص سرورهای ابری را بدون حذف موارد پیشفرض اضافه کرد.System.Data.SqlClient به Microsoft.Data.SqlClient دیگر یک پیشنهاد اختیاری نیست، بلکه پیشنیازی ضروری برای سیستمهای مدرن داتنت به شمار میرود. قابلیت Configurable Retry Logic یک مکانیزم سبک، درونی و به شدت آگاه به جزئیات SQL Server را فراهم میکند که نیاز به چارچوبهای جانبی را برای کنترل خطاهای موقت برطرف میسازد. به کارگیری صحیح این قابلیت - با در نظر گرفتن پایستگی عملیات و ملاحظات تراکنشی - پایداری سیستمهای نرمافزاری را در مقیاسهای بزرگ و محیطهای ابری به صورت چشمگیری ارتقا میدهد.ExecutionStrategy در Entity Framework Core) و دیگری در پایینترین سطح انتزاع درایور شبکه و پروتکل TDS (Configurable Retry Logic در Microsoft.Data.SqlClient). اگرچه هدف نهایی هر دو، تابآوری سیستم در برابر خطاهای زودگذر است، اما تفاوتهای عمیق معماری در سطح بافتار (Context Awareness)، شیوه مدیریت وضعیت (State Management)، تراکنشها و محل وقوع خطا میان این دو وجود دارد.| معیار مقایسه | ExecutionStrategy در EF Core | Configurable Retry در SqlClient |
| لایه انتزاعی (Layer of Abstraction) | لایه ORM / Application Domain (سطح بالا) | لایه ADO.NET / TDS Protocol Driver (سطح پایین) |
| مدیریت وضعیت شیء (ChangeTracker) | کاملاً آگاه از وضعیت Entityها؛ در صورت شکست SaveChanges، وضعیت Tracker برای تلاش بعدی حفظ یا بازنشانی میشود. | کاملاً بیاطلاع از موجودیتهای لایه دامین و صرفاً ناظر بر بایتهای سوکت و دستورات SQL. |
| مدیریت تراکنشهای کاربر (User Transactions) | از طریق متد CreateExecutionStrategy().Execute() کل بلوک تراکنش از ابتدا تا انتها مجدداً اجرا میشود. | دستورات درون تراکنش را بازآزمایی نمیکند (تراکنش در سطح سرور Rollback یا نامعتبر میشود). |
بازآزمایی باز کردن کانکشن (OpenAsync) | توسط استراتژی پوشش داده میشود (بخشی از فرآیند اجرای دستور یا تراکنش). | مستقیماً در متد OpenAsync() و بدون نیاز به ایجاد کانتکست یا تراکنش بازآزمایی میشود. |
| دامنه سازگاری (Compatibility) | صرفاً محدود به کدهایی که از DbContext استفاده میکنند. | عمومی و قابل استفاده در Dapper، ADO.NET خالص، ابزارهای BulkCopy و Micro-ORMها. |
| سربار پردازشی (Overhead) | بیشتر؛ نیازمند ساخت Delegates، بازبینی تغییرات و ردیابی وضعیت اشیاء. | بسیار ناچیز و کمینه؛ بررسی مستقیم کد خطای TDS و تاخیر در همان سوکت فعال. |
SqlClientدر تراکنشهاINSERT INTO Orders ...UPDATE Inventory ... (شکست به دلیل قطعی موقت یا Deadlock)COMMITSqlClient نمیتواند دستور شماره ۲ را به تنهایی Retry کند؛ زیرا به محض قطعی ارتباط یا وقوع خطای بنبست (Deadlock Error 1205)، موتور SQL Server کل نشست یا تراکنش را لغو (Abort/Rollback) میکند. تلاش مجدد فقط برای دستور ۲ باعث بروز خطای Transaction has completed; it is usable no longer میشود. به همین دلیل درایور هوشمندانه دستورات دارای Transaction فعال را نادیده میگیرد.EF Core Execution Strategyدر تراکنشهاvar strategy = dbContext.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(async () =>
{
using var transaction = await dbContext.Database.BeginTransactionAsync();
// ۱. ثبت سفارش
dbContext.Orders.Add(new Order { Total = 100 });
await dbContext.SaveChangesAsync();
// ۲. کاهش موجودی انبار
var product = await dbContext.Products.FindAsync(productId);
product.Stock -= 1;
await dbContext.SaveChangesAsync();
await transaction.CommitAsync();
});EnableRetryOnFailure(maxRetryCount: 5) در EF CoreRetryLogicProvider با NumberOfTries = 5 به کانکشن SqlClient زیرینEF Core ExecutionStrategyChangeTracker، مدیریت بازآزمایی SaveChanges، و پشتیبانی از ExecuteAsync برای بلوکهای تراکنشی.SqlClient غیرفعال بگذارید.SqlClient Configurable RetrySqlClient سبکترین و نزدیکترین نقطه به شبکه برای کنترل قطعیهای لحظهای است.SqlRetryLogicBaseProvider بهصورت Singleton بسازید و به تمام کانکشنها و کامندهای فاقد تراکنش اختصاص دهید.EnableRetryOnFailure را برای عملیاتهای EF فعال کنید.RetryLogicProvider دارند؛ اما هرگز کانکشن مشترک مدیریتشده توسط EF Core را به Provider مجهز نکنید تا از اثر تداخلی جلوگیری شود.SqlClient Configurable Retry روی لایه انتقال (Transport Layer) و پروتکل تمرکز دارد؛ بسیار سریع و بهینه است اما از منطق دامین و تراکنشهای چندمرحلهای بیخبر است.EF Core ExecutionStrategy روی لایه عملیات دامین و تراکنش (Business Unit of Work) تمرکز دارد؛ توانایی مدیریت وضعیتهای پیچیده و Rollback کل فرآیند را دارد اما سربار بیشتری به برنامه تحمیل میکند.