Note: see the link below for the English version of this article.
https://duongnt.com/polly-custom-policy
Trong bài trước, ta đã dùng Polly để thử gửi lại các request HTTP. Như đã nói trong phần Kết thúc, hôm nay chúng ta sẽ tìm hiểu một tính năng khó hơn: custom policy.
Có lẽ các bạn còn nhớ ta đã dùng hàm Policy.TimeoutAsync để tạo IAsyncPolicy
rồi dùng policy đó để ngừng việc đợi một request chạy chậm. Trong bài hôm nay, chúng ta sẽ tự viết policy của riêng mình.
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
Một vấn đề cần sử dụng đến thử lại
Như đã nhắc đến trong bài trước, Polly không chỉ được dùng để thử gửi lại các request HTTP. Hôm nay, chúng ta sẽ dùng nó để giải quyết một vấn đề khác cũng cần đến việc thử lại.
Sinh ngẫu nhiên số trong khoảng xác định chỉ bằng một đồng xu
Ta sẽ giải một bài toán nổi tiếng. Cho một hàm có khả năng trả về 0
hoặc 1
với cùng xác suất là 50% (giống như ta tung đồng xu), làm thế nào để sinh ngẫu nhiên số trong một khoảng cho trước với điều kiện là xác suất sinh từng số trong khoảng đó là bằng nhau?
Phương pháp để giải bài toán này là như sau.
- Tìm số
n
nhỏ nhất sao cho2^n - 1>= cận trên
. - Tung đồng xu
n
lần để thu vền
bit. - Chuyển số nhị phân với
n
bit ở trên sang dạng thập phân. - Nếu giá trị thu được nằm trong khoản cho trước thì ta trả về giá trị đó.
- Còn nếu không thì ta bỏ kết quả đó đi và thử lại từ bước 1.
Phiên bản đầu tiên
Code ban đầu của ta là như sau.
var bitCount = (int)Math.Floor(Math.Log(max, 2)) + 1;
var rs = 0;
for (var bitIndex = 0; bitIndex < bitCount; bitIndex++)
{
var bit = _bitGetter.Get(); // _bitGetter là hàm trả về 0 hoặc 1
rs += (int)Math.Pow(2, bitIndex) * bit;
}
return rs;
Rõ ràng là hàm này có thể sinh số bất kỳ giữa 0
và 2^n - 1
với xác suất bằng nhau. Mục tiêu hôm nay của ta là tạo một custom policy trong Polly để thực hiện việc thử lại mỗi khi rs
nằm ngoài khoảng cho trước.
Tạo custom policy với Polly
Custom policy đơn giản nhất
Polly cho phép tạo custom policy một cách dễ dàng. Để xử lý các delegate đồng bộ, ta chỉ cần kế thừa lớp trừu tượng Policy
và viết lại hàm Implementation
. Trong trường hợp đơn giản nhất, policy của ta trông như sau.
public class PassthroughPolicy : Policy
{
protected override TResult Implementation<TResult>(Func<Context, CancellationToken, TResult> action,
Context context, CancellationToken cancellationToken)
{
return action(context, cancellationToken);
}
}
Ở đây, action
chính là delegate mà ta truyền vào hàm Execute
của ISyncPolicy
. PassthroughPolicy
sẽ chỉ gọi delegate và trả về kết quả mà không thêm bớt gì.
Kiểm tra xem số được sinh ra có nằm trong khoảng cho trước không
Khi khởi tạo policy, ta lưu cận trên và cận dưới vào hai trường private. Chú ý là ta đặt hàm khởi tạo cũng là private rồi định nghĩa một hàm static để gọi hàm khởi tạo đó. Đây là quy ước thường được sử dụng của Polly.
private readonly int _min, _max;
private NumberOutOfRangePolicy(int min, int max)
{
_min = min;
_max = max;
}
public static NumberOutOfRangePolicy Create(int min, int max) =>
new NumberOutOfRangePolicy(min, max);
Bước tiếp theo là viết code để kiểm tra xem số được sinh ra bởi action
có nằm trong khoảng cho trước không.
var num = action(context, cancellationToken);
if (num as int? < _min || num as int? > _max)
{
throw new NumberOutOfRangeException();
}
return num;
Thử lại khi số được sinh nằm ngoài khoảng cho trước
Có thể thấy là policy của ta sẽ tạo NumberOutOfRangeException
khi số được sinh ngẫu nhiên nằm ngoài khoảng cho trước. Sau đó, ta dùng một policy khác để xử lý ngoại lệ đó và thực hiện việc thử lại. Vì delegate ở đây là đồng bộ nên ta dùng Policy.Wrap
và Execute
thay vì dùng Policy.WrapAsync
và ExecuteAsync
var numberOutOfRangePolicy = NumberOutOfRangePolicy.Create(min, max);
var retryPolicy = Policy
.Handle<NumberOutOfRangeException>()
.RetryForever(); // Theo lý thuyết xác suất, bước này sẽ có lúc thành công
_policies = Policy.Wrap(retryPolicy, numberOutOfRangePolicy);
_policies.Execute(() => numberGenerator.Next(91, 100));
Tất nhiên là ta có thể viết code để thực hiện việc thử lại ngay trong NumberOutOfRangePolicy
. Lúc này ta không cần đến retryPolicy
nữa.
TResult num;
do
{
num = action(context, cancellationToken);
}
while (num as int? <= _min && num as int? >= _max);
return num;
Nhưng theo tôi ta không nên dùng cách này. Tại sao phải đi làm lại việc người khác đã làm?
Thử hàm sinh số ngẫu nhiên với policy
Dưới đây là kết quả của 1000 lần gọi hàm numberGenerator.Next(91, 100)
có sử dụng policy và không dùng policy.
######################################
Generating numbers without policy...
######################################
0: 0.5%
1: 0.5%
2: 0.8%
3: 0.4%
// ...lược bỏ bớt nhiều dòng
126: 0.8999999999999999%
127: 0.8%
######################################
Generating numbers with policy...
######################################
91: 10.7%
92: 8.9%
93: 10%
94: 9.1%
95: 9.8%
96: 10.2%
97: 9.6%
98: 10%
99: 11.600000000000001%
100: 10.100000000000001%
Khi không dùng policy, ta sinh tất cả các số giữa 0
và 127 (2^7 - 1)
với cùng xác suất là 1/128
. Còn khi dùng policy, ta chỉ sinh số nằm giữa 91
và 100
, mỗi số có xác suất là 1/10
.
Cách tạo custom policy không đồng bộ
Viết custom policy không đồng bộ
Để tạo custom policy không đồng bộ, ta cần kế thừa lớp trừu tượng AsyncPolicy
. Để hiểu rõ hơn, ta sẽ tạo một policy có khả năng ngừng việc đợi một hàm không đồng bộ chạy chậm. Policy của ta gần giống với Policy.TimeoutAsync
được giới thiệu trong bài trước. Các bạn có thể tham khảo code hoàn chỉnh tại đây. Còn dưới đây là hàm ImplementationAsync
.
private readonly int _timeoutInMilliSecs;
protected override async Task<TResult> ImplementationAsync<TResult>(
Func<Context, CancellationToken, Task<TResult>> action, Context context,
CancellationToken cancellationToken, bool continueOnCapturedContext)
{
var delegateTask = action(context, cancellationToken);
var timeoutTask = Task.Delay(_timeoutInMilliSecs);
await Task.WhenAny(delegateTask, timeoutTask).ConfigureAwait(continueOnCapturedContext);
if (timeoutTask.IsCompleted)
{
throw new DuongTimeoutException(
$"{context.OperationKey}: Task did not complete within: {_timeoutInMilliSecs}ms");
}
return await delegateTask;
}
Tất nhiên policy của ta còn rất thô sơ và không phù hợp để sử dụng trong thực tế. Tuy nhiên nó đủ để làm ví dụ. Ta dùng Task.Delay
để tạo một task, task này kết thúc sau _timeoutInMilliSecs
. Nếu task đó chạy xong trước task tạo bởi action
thì ta biết là action
đã chạy lâu hơn _timeoutInMilliSecs
và ta cần tạo lỗi.
Thử nghiệm policy mới
Dưới đây là code dùng policy vừa tạo để ngừng việc đợi một task chạy lâu hơn 500ms
.
var pollyContext = new Context("Timeout");
var policy = DuongTimeoutPolicy.Create(500);
try
{
await policy.ExecuteAsync(async ctx =>
{
await Task.Delay(1000);
}, pollyContext);
Console.WriteLine("Task ran to completion.");
}
catch (DuongTimeoutException ex)
{
Console.WriteLine(ex.Message);
}
Đoạn code trên sẽ cho kết quả như sau.
Timeout: Task did not complete within: 500ms
Nếu ta muốn thử thực hiện lại task này, ta có thể thêm một policy mới để xử lý DuongTimeoutException
.
Kết thúc
Cả hai ví dụ trong bài hôm nay vẫn là tương đối đơn giản. Nếu các bạn có sáng kiến nào thì hãy cho tôi biết qua phần bình luận.