Chìa khóa để thực hiện lại việc thử lại này là deferred observables. Một quan sát được hoãn lại sẽ không thực thi nhà máy của nó cho đến khi ai đó đăng ký với nó và nó sẽ gọi nhà máy cho mỗi đăng ký, làm cho nó trở nên lý tưởng cho việc thử lại của chúng tôi kịch bản.
Giả sử chúng ta có một phương pháp mà gây nên một yêu cầu mạng.
public IObservable<WebResponse> SomeApiMethod() { ... }
theo mục đích của đoạn này chút, chúng ta hãy xác định trì hoãn như source
var source = Observable.Defer(() => SomeApiMethod());
Bất cứ khi nào ai đó đăng ký nguồn, nó sẽ gọi SomeApiMethod và khởi chạy một yêu cầu web mới. Cách ngây thơ để thử lại nó bất cứ khi nào nó không thành công sẽ được sử dụng trong xây dựng trong nhà điều hành thử lại.
source.Retry(4)
Điều đó sẽ không tốt cho API mặc dù nó không phải là những gì bạn đang yêu cầu. Chúng ta cần trì hoãn việc khởi chạy các yêu cầu giữa mỗi lần thử. Một cách để làm điều đó là với một delayed subscription.
Observable.Defer(() => source.DelaySubscription(TimeSpan.FromSeconds(1))).Retry(4)
Đó không phải là lý do vì nó sẽ thêm độ trễ ngay cả trong yêu cầu đầu tiên, chúng ta hãy khắc phục điều đó.
int attempt = 0;
Observable.Defer(() => {
return ((++attempt == 1) ? source : source.DelaySubscription(TimeSpan.FromSeconds(1)))
})
.Retry(4)
.Select(response => ...)
Chỉ tạm dừng cho giây không phải là phương pháp thử lại rất tốt mặc dù vậy hãy thay đổi hằng số thành hàm nhận số lần thử lại và trả về độ trễ thích hợp. Trả về theo hàm mũ là đủ dễ thực hiện.
Func<int, TimeSpan> strategy = n => TimeSpan.FromSeconds(Math.Pow(n, 2));
((++attempt == 1) ? source : source.DelaySubscription(strategy(attempt - 1)))
Chúng tôi gần như hoàn tất ngay bây giờ, chúng tôi chỉ cần thêm cách chỉ định ngoại lệ mà chúng tôi nên thử lại. Hãy thêm một hàm cho một ngoại lệ trả về việc có thử lại hay không, chúng ta sẽ gọi nó là retryOnError.
Bây giờ chúng ta cần viết một số mã tìm kiếm đáng sợ nhưng phải chịu đựng với tôi.
Observable.Defer(() => {
return ((++attempt == 1) ? source : source.DelaySubscription(strategy(attempt - 1)))
.Select(item => new Tuple<bool, WebResponse, Exception>(true, item, null))
.Catch<Tuple<bool, WebResponse, Exception>, Exception>(e => retryOnError(e)
? Observable.Throw<Tuple<bool, WebResponse, Exception>>(e)
: Observable.Return(new Tuple<bool, WebResponse, Exception>(false, null, e)));
})
.Retry(retryCount)
.SelectMany(t => t.Item1
? Observable.Return(t.Item2)
: Observable.Throw<T>(t.Item3))
Tất cả những dấu ngoặc nhọn đang có để sắp xếp một ngoại lệ mà chúng ta không nên thử lại quá khứ .Retry()
. Chúng tôi đã làm cho các bên trong quan sát được một IObservable<Tuple<bool, WebResponse, Exception>>
nơi bool đầu tiên chỉ ra nếu chúng ta có một phản ứng hoặc một ngoại lệ. Nếu retryOnError chỉ ra rằng chúng ta nên thử lại cho một ngoại lệ đặc biệt, quan sát bên trong sẽ ném và sẽ được chọn bằng cách thử lại. SelectMany chỉ mở gói Tuple của chúng ta và làm cho kết quả có thể quan sát được là IObservable<WebRequest>
một lần nữa.
Xem gist with full source and tests của tôi cho phiên bản cuối cùng. Có điều hành này cho phép chúng ta viết mã retry của chúng tôi khá ngắn gọn
Observable.Defer(() => SomApiMethod())
.RetryWithBackoffStrategy(
retryCount: 4,
retryOnError: e => e is ApiRetryWebException
)
lớn thứ Markus. –
Dường như với tôi, việc triển khai này không thể hủy đăng ký. Đó là một chút khó khăn để dán ở đây, nhưng hãy thử điều này, bạn sẽ thấy khoảng thời gian giữ ticking: Observable.Interval (TimeSpan.FromSeconds (1)) Do (Console.WriteLine) .RetryWithBackoffStrategy() Take (1). Đăng ký(); –
@NiallConnaughton Đẹp bắt! Lý do mà nó không hủy đăng ký từ nguồn đã phải làm với tôi ban đầu đã mô hình hóa phương pháp sau khi một nhà điều hành nội bộ khác, chúng tôi có, một trong đó sản xuất quan sát nóng. Nhà điều hành này không nên làm điều đó. Tôi đã thay đổi mã để tạo ra các quan sát lạnh và thêm một thử nghiệm để xác minh rằng nó hủy đăng ký. Cảm ơn! –