Note: see the link below for the English version of this article.
https://duongnt.com/polly-retry-request
Đôi khi, các request HTTP của ta gặp phải lỗi nhất thời. Thông thường, nếu ta gửi lại request vào một thời điểm khác thì những lỗi này sẽ tự hết. Mặc dù có thể tự viết code để gửi lại request, tôi ưu tiên sử dụng thư viện chuyên dụng như Polly cho việc này.
Các bạn có thể tải code ví dụ trong bài từ đường link dưới đây.
https://github.com/duongntbk/PollyDemo
Tự mình gửi lại request
Giả sử thỉnh thoảng ta gặp lỗi 503-Service Unavailable
khi gọi API. Ta muốn gửi lại request trong trường hợp đó, nhưng ta cần giới hạn số lần gửi một request là không quá 5 lần. Code thực hiện việc này có vẻ không quá phức tạp.
private readonly HttpClient _client = new HttpClient();
public async Task<HttpResponseMessage> SendRequestWithRetry()
{
HttpResponseMessage response;
var sendCount = 0;
do
{
var request = new HttpRequestMessage(HttpMethod.Get, new Uri("https://www.example.com"));
response = await _client.SendAsync(request);
sendCount++;
}
while (sendCount < 5 && response.StatusCode == System.Net.HttpStatusCode.ServiceUnavailable);
return response;
}
Nhưng ta khó tái sử dụng code trên cho những endpoint khác. Có lẽ ta cần tách phần logic xử lý request HTTP sang một class chuyên dụng và viết logic gửi lại request trong hàm SendRequest
của class mới này.
Hơn nữa, ta không nên gửi lại request ngay sau khi có lỗi. Thay vào đó, ta nên đợi một lúc để API có thời gian phục hồi. Có lẽ các bạn đã nhận ra vấn đề. Việc đáp ứng tất cả các yêu cầu trên là tương đối tốn thời gian và dễ gây nhầm lẫn. Vì thế, ta nên sử dụng những thư viện chuyên dụng như Polly.
Gửi lại request bằng thư viện Polly
Bước chuẩn bị
Các bạn có thể tải Polly bằng lệnh dưới.
dotnet add package Polly
Ta cũng cần một API để test. Tôi đã viết một API đơn giản để làm ví dụ cho bài hôm nay, các bạn hãy tham khảo đường link này. Để chạy API, các bạn cần vào folder DummyApi
và chạy lệnh dưới đây.
dotnet run
Đường dẫn tới API là https://localhost:5001.
Polly hoạt động như thế nào
Nói một cách tổng quát, ta sẽ gửi request bằng Polly thông qua 2 interface: ISyncPolicy
dành cho lệnh đồng bộ và IAsyncPolicy
dành cho lệnh không đồng bộ. Trong bài hôm nay, ta chỉ quan tâm tới interface IAsyncPolicy
. Cụ thể hơn, ta sẽ dùng hàm dưới đây.
Task<TResult> ExecuteAsync<TResult>(Func<Context, Task<TResult>> action, Context context);
Khi dùng hàm này, ta có thể đơn giản hóa logic để gửi lại request như sau.
var policy = <code to create an object that implements IAsyncPolicy>;
var pollyContext = new Context('<tên cho context>');
var response = await policy.ExecuteAsync(async ctx =>
{
var request = new HttpRequestMessage(HttpMethod.Get, new Uri("https://www.example.com"));
return await _client.SendAsync(request);
}
Đó là tất cả những điều ta cần làm. Bằng cách thiết lập object policy
, ta có thể làm nhiều điều, ví dụ như.
- Xử lý một kiểu exception nhất định, hoặc xử lý exception có một property nhất định.
- Thử gửi lại request tối đa một số lần nhất định, giữa 2 lần gửi request có thời gian chờ.
- Ngừng đợi request nếu thời gian chạy là quá lâu.
- Lưu response vào cache.
- Giới hạn tần suất gửi request.
- Kết hợp của 2 hay nhiều việc trên.
Thử gửi lại request nếu gặp một lỗi nhất định
Code ta sử dụng
Trong ví dụ đầu tiên, ta sẽ dùng Polly để gửi lại request nếu gặp lỗi 503. Số lần thử lại tối đa là 5 lần. Đồng thời, ta đợi 500 ms giữa 2 lần gửi.
var pollyContext = new Context("Retry 503");
var policy = Policy
.Handle<HttpRequestException>(ex => ex.Message.Contains("503"))
.WaitAndRetryAsync(
5,
_ => TimeSpan.FromMilliseconds(500),
(result, timespan, retryNo, context) =>
{
Console.WriteLine($"{context.OperationKey}: Retry number {retryNo} within " +
$"{timespan.TotalMilliseconds}ms. Original status code: 503");
}
);
var response = await policy.ExecuteAsync(async ctx =>
{
var request = new HttpRequestMessage(HttpMethod.Get, new Uri("https://www.example.com"));
var response = await _client.SendAsync(request);
response.EnsureSuccessStatusCode();
return response;
}, pollyContext);
Đoạn code trên làm những gì?
Ta sẽ xét từng dòng trong đoạn code trên để xem chúng làm gì.
Hàm Handle<T>
quyết định policy của ta có thể xử lý những sự kiện gì. Trong trường hợp này, ta dùng HttpRequestException
làm kiểu generic, đồng thời ta truyền vào hàm một delegate để lọc ra những HttpRequestException
có status code là 503-Service Unavailable
.
.Handle<HttpRequestException>(ex => ex.Message.Contains("503"))
Hàm WaitAndRetryAsync
định nghĩa hành động ta muốn thực hiện. Ở đây, ta sẽ thử gửi lại request sau một khoảng chờ. Hàm này có rất nhiều overload, nhưng ta sẽ sử dụng phiên bản đơn giản với 3 tham số.
retryCount
: số lần tối đa ta gửi lại request. Ta đặt giá trị này là5
.Func<int, TimeSpan> sleepDurationProvider
: delegate để trả về thời gian chờ giữa 2 lần gửi request. Giá trị này được tính dựa vào số lần request đã được gửi trước đó. Ở đây, ta cố định thời gian chờ là500 ms
. Nhưng nếu cần ta có thể tăng dần thời gian chờ sau mỗi lần request gặp lỗi._ => TimeSpan.FromMilliseconds(500)
Action<Exception, TimeSpan, int, Context> onRetry
: những lệnh ta thực hiện trước khi gửi lại request. Ở đây ta chỉ đơn giản là ghi lại log.(ex, timespan, retryNo, context) => { Console.WriteLine($"{context.OperationKey}: Retry number {retryNo} within " + $"{timespan.TotalMilliseconds}ms. Original status code: 503"); }
Có lẽ các bạn cũng đã nhận ra là ta gọi hàm EnsureSuccessStatusCode
bên trong delegate của ExecuteAsync
. Nguyên nhân là do HttpClient
không sinh exception khi server trả về 503-Service Unavailable
. Trong khi đó, kiểu T
của hàm Handle<T>
lại phải là exception. Vì thế, ta phải tự mình sinh HttpRequestException
.
response.EnsureSuccessStatusCode();
Chạy thử đoạn code trên
Đường dẫn /demo/error/{requestId}/{errorCode}
của API test luôn trả về errorCode
trong lần đầu tiên ta gọi nó với từng requestId
. Những lần gọi sau với cùng requestId
sẽ trả về 200-OK
.
var requestId = Guid.NewGuid();
var errorCode = 503;
var response = await policy.ExecuteAsync(async ctx =>
{
var request = new HttpRequestMessage(HttpMethod.Get, new Uri($"https://localhost:5001/demo/error/{requestId}/{errorCode}"));
var response = await _client.SendAsync(request);
response.EnsureSuccessStatusCode();
return response;
}, pollyContext);
Console.WriteLine(await response.Content.ReadAsStringAsync());
Đoạn code trên sẽ in ra thông tin dưới đây.
Retry503: Retry number 1 within 503ms. Original status code: 503 // request đầu tiên trả về 503 và được gửi lại một lần nữa
be21236d-6eb7-4a58-8208-56ac1dee4207 did not throw an error. // lần gửi thứ 2 thì request đã thành công
Thử lại tất cả các lỗi nhất thời
Đoạn code ở phần trên cho phép ta tự do chọn xử lý những status code nào. Nhưng nếu ta muốn xử lý tất cả những lỗi nhất thời thì ta sẽ phải viết một delegate phức tạp cho hàm Handle<T>
. Thay vào đó, ta có thể dùng hàm HandleTransientHttpError
trong thư viện Polly.Extensions.Http
. Hàm này cho phép ta xử lý tất cả các lỗi 5xx
và cả lỗi 408-Request Timeout
.
var policy = HttpPolicyExtensions
.HandleTransientHttpError() // <- hàm extension từ Polly.Extensions.Http
.WaitAndRetryAsync(
// lược bỏ một phần code
);
Code sử dụng policy trên cũng đơn giản hơn. Chú ý là ta không cần gọi EnsureSuccessStatusCode
nữa.
var response = await policy.ExecuteAsync(async ctx =>
{
var request = new HttpRequestMessage(HttpMethod.Get, new Uri($"https://localhost:5001/demo/error/{requestId}/{errorCode}"));
return await _client.SendAsync(request);
}, pollyContext);
Console.WriteLine(await response.Content.ReadAsStringAsync());
Đoạn code trên sẽ in ra thông tin dưới đây.
RetryError: Retry number 1 after 500ms. Original status code: ServiceUnavailable // request đầu tiên trả về 503 và được gửi lại một lần nữa
be21236d-6eb7-4a58-8208-56ac1dee4207 did not throw an error. // lần gửi thứ 2 thì request đã thành công
Ngừng đợi một request chạy lâu
Ta không nhất thiết phải dùng hàm WaitAndRetryAsync
khi tạo policy. Giả sử rằng request của ta đôi khi chạy rất mất thời gian và ta muốn ngừng đợi nó sau một khoảng chờ nhất định. Hàm TimeoutAsync
có thể giúp ta thực hiện việc này.
var timeoutInMilliSecs = 200; // Ngừng đợi request sau 200 ms
var policy = Policy.TimeoutAsync(TimeSpan.FromMilliseconds(timeoutInMilliSecs), TimeoutStrategy.Pessimistic);
Nếu request thực hiện bằng policy này chạy lâu hơn 200 ms
, Polly sẽ tự động sinh ra TimeoutRejectedException
.
Ta sẽ thử policy này bằng cách gọi /demo/error/{requestId}/{delayInMilliSecs}
. Đường dẫn này sẽ đợi ít nhất delayInMilliSecs
trước khi trả về kết quả trong lần gọi đầu tiên với từng requestId
. Những lần gọi sau với cùng requestId
sẽ trả lại kết quả ngay lập tức.
// Đoạn code dưới sẽ gặp TimeoutRejectedException
var requestId = Guid.NewGuid();
var delay = 1000; // API sẽ đợi ít nhất 1000 ms trước khi trả lại kết quả
var response = await policy.ExecuteAsync(async ctx =>
{
var request = new HttpRequestMessage(HttpMethod.Get, new Uri($"https://localhost:5001/demo/delay/{requestId}/{delay}"));
return await _client.SendAsync(request);
}, pollyContext);
Kết họp nhiều policy của Polly cùng lúc
Trong phần này, chúng ta sẽ ngừng đợi các request chạy lâu rồi thử gửi lại nó. Ta đã biết cách riêng lẻ làm từng yêu cầu trên, nhưng làm thế nào để thực hiện cả hai cùng lúc? Câu trả lời là ta cần dùng hàm Policy.WrapAsync
. Hàm này cho phép ta tạo một object AsyncPolicyWrap
để chứa tất cả các IAsyncPolicy
ta truyền vào. Và vì bản thân nó cũng implement IAsyncPolicy
, ta không phải thay đổi code nhiều.
var timeoutPolicy = Policy.TimeoutAsync(TimeSpan.FromMilliseconds(timeoutInMilliSecs), TimeoutStrategy.Pessimistic);
var retryPolicy = Policy
.Handle<TimeoutRejectedException>() // chú ý là ở đây ta cần xử lý TimeoutRejectedException
.WaitAndRetryAsync(
// lược bỏ một phần code
);
var wrapper = Policy.WrapAsync(retryPolicy, timeoutPolicy);
var response = await wrapper.ExecuteAsync(async ctx =>
{
var request = new HttpRequestMessage(HttpMethod.Get, new Uri($"https://localhost:5001/demo/delay/{requestId}/{delay}"));
return await _client.SendAsync(request);
}, pollyContext);
Console.WriteLine(await response.Content.ReadAsStringAsync());
Ở đây có một điểm đáng lưu ý. Đó là khi gọi WrapAsync
, ta cần truyền retryPolicy
trước khi truyền timeoutPolicy
. Nguyên nhân là vì AsyncPolicyWrap
sẽ bọc policy bên trái ra ngoài policy bên phải. Nếu ta viết là (retryPolicy, timeoutPolicy)
thì đầu tiên timeoutPolicy
sẽ ngừng đợi request chạy lâu bằng cách sinh TimeoutRejectedException
. Sau đó, retryPolicy
sẽ thử gửi lại những request nào có TimeoutRejectedException
. Nếu ta đảo ngược thứ tự thì retryPolicy
sẽ được thực hiện trước, và nó sẽ không tìm thấy TimeoutRejectedException
nào cả.
Đoạn code trên sẽ in ra thông tin dưới đây.
RetryDelay: Retry number 1 after 500ms because The delegate executed asynchronously through TimeoutPolicy did not complete within the timeout. // Request này chạy lâu hơn 200 ms, bị timeout, rồi được thử lại
be21236d-6eb7-4a58-8208-56ac1dee4207 was not delayed. // lần gửi request thứ 2 sẽ thành công
Kết thúc
Thư viện Polly giúp việc thử gửi lại request trở nên dễ dàng hơn. Nhưng thư viện này còn làm được nhiều hơn thế. Từ các tham số của ExecuteAsync
, có lẽ các bạn cũng đoán được là nó không chỉ biết cách xử lý các request HTTP. Trong bài tới, chúng ta sẽ tìm hiểu cách viết custom policy bằng Policy.
One Thought on “Dùng Polly để thử gửi lại request trong C#”