Retry Logic ถูก Implement เมื่อมี Operation ที่ล้มเหลว ควร Implement Retry Logic เฉพาะที่มี Full Context ของ Failing Operation
สำคัญที่จะ Log Connectivity Failures ทั้งหมดที่ทำให้เกิด Retry เพื่อให้ระบุปัญหา Root Cause ของ Application, Services, หรือ Resources ได้
ตัวอย่างพื้นฐาน: Retry Helper
using System;
using System.Net.Http;
using System.Threading.Tasks;
class Program {
public static async Task Main() {
using var client = new HttpClient();
var retryAttempts = 3;
var delay = TimeSpan.FromSeconds(2);
var result = await RetryHelper.RetryAsync(
retryAttempts,
delay,
() => client.GetAsync("https://api.example.com/data"),
cancellationToken: CancellationToken.None
);
Console.WriteLine($"Status: {result.StatusCode}");
Console.ReadLine();
}
}
public static class RetryHelper {
public static async Task RetryAsync(
int maxAttempts,
TimeSpan delay,
Func> operation,
CancellationToken cancellationToken = default) {
for (int attempt = 1; ; attempt++) {
try {
Console.WriteLine($"Attempt {attempt}/{maxAttempts}");
return await operation();
}
catch (Exception ex) when (attempt < maxAttempts) {
Console.WriteLine($"Attempt {attempt} failed: {ex.Message}. Retrying in {delay.TotalSeconds}s...");
await Task.Delay(delay, cancellationToken);
}
}
}
}
Exponential Backoff (แนะนำสำหรับ Production)
public static class RetryHelper {
public static async Task RetryWithBackoffAsync(
int maxAttempts,
TimeSpan baseDelay,
Func> operation,
double multiplier = 2.0,
TimeSpan? maxDelay = null,
CancellationToken cancellationToken = default) {
var delay = baseDelay;
for (int attempt = 1; ; attempt++) {
try {
return await operation();
}
catch (Exception ex) when (attempt < maxAttempts) {
Console.WriteLine($"Attempt {attempt} failed: {ex.Message}. Waiting {delay.TotalSeconds}s...");
await Task.Delay(delay, cancellationToken);
delay = TimeSpan.FromSeconds(Math.Min(
delay.TotalSeconds * multiplier,
maxDelay?.TotalSeconds ?? double.MaxValue
));
}
}
}
}
// Usage
var result = await RetryHelper.RetryWithBackoffAsync(
maxAttempts: 5,
baseDelay: TimeSpan.FromSeconds(1),
operation: () => httpClient.GetAsync(url),
multiplier: 2.0,
maxDelay: TimeSpan.FromMinutes(1)
);
Polly Library (Enterprise-grade Resilience)
// NuGet: Polly
using Polly;
using Polly.Retry;
// Retry Policy
var retryPolicy = Policy
.Handle()
.Or()
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)),
onRetry: (outcome, timespan, retryCount, context) => {
Console.WriteLine($"Retry {retryCount} after {timespan.TotalSeconds}s: {outcome.Exception?.Message}");
});
// Circuit Breaker
var circuitBreaker = Policy
.Handle()
.CircuitBreakerAsync(
exceptionsAllowedBeforeBreaking: 5,
durationOfBreak: TimeSpan.FromSeconds(30));
// Combined Policy
var policy = Policy.WrapAsync(retryPolicy, circuitBreaker);
// Execute
var response = await policy.ExecuteAsync(() => httpClient.GetAsync(url));
HttpClient に Polly 登録 (.NET Core DI)
// Startup.cs / Program.cs
services.AddHttpClient("MyApi", client => {
client.BaseAddress = new Uri("https://api.example.com/");
})
.AddTransientHttpErrorPolicy(p => p.WaitAndRetryAsync(3, _ => TimeSpan.FromSeconds(2)))
.AddTransientHttpErrorPolicy(p => p.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));
// Usage
var client = httpClientFactory.CreateClient("MyApi");
var response = await client.GetAsync("/data");
Retry Best Practices
| Practice | Description |
|---|---|
| Idempotency | Retry เฉพาะ Idempotent Operations (GET, PUT, DELETE) ไม่ใช่ POST |
| Exponential Backoff | เพิ่ม Delay แบบ Exponential ป้องกัน Thundering Herd |
| Jitter | เพิ่ม Randomness ใน Delay ป้องกัน Synchronized Retries |
| Max Retries | จำกัด Retry Count (3-5 ครั้ง) |
| Max Delay | Cap Delay สูงสุด (e.g., 1 นาที) |
| Cancellation Token | รองรับ Cancellation สำหรับ Timeout/Shutdown |
| Logging | Log ทุก Retry Attempt พร้อม Context |
| Circuit Breaker | หยุดส่ง Request เมื่อ Failure Rate สูง |
Non-Idempotent Operations (POST) - ควรระวัง
// POST อาจทำให้ Duplicate Resources
// ใช้ Idempotency Key
var request = new HttpRequestMessage(HttpMethod.Post, "/orders");
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
request.Content = JsonContent.Create(order);
var response = await policy.ExecuteAsync(() => client.SendAsync(request));