2012-05-07 19 views
10

Tất cả các cuộc gọi dịch vụ trong ứng dụng của tôi được thực hiện dưới dạng tác vụ.Khi có công việc bị lỗi, tôi cần trình bày cho người dùng một hộp thoại để thử lại hoạt động cuối cùng thất bại.Nếu người dùng chọn thử lại chương trình sẽ thử lại tác vụ, nếu không thì việc thực thi chương trình sẽ tiếp tục sau khi ghi ngoại lệ. Bất kỳ ai có ý tưởng cấp cao về cách triển khai chức năng này?Thử lại tác vụ nhiều lần dựa trên đầu vào của người dùng trong trường hợp ngoại lệ trong tác vụ

+0

@svick tôi đã không cố gắng để thực hiện chức năng này đó là một nhiệm vụ tương lai sắp tới, trông giống như chúng tôi đã nhận những ý tưởng thú vị đã có, đã đi qua chúng một cách chi tiết và cung cấp cho nó một thử –

Trả lời

31

CẬP NHẬT 5/2017

C# 6 bộ lọc ngoại lệ làm cho catch khoản đơn giản hơn rất nhiều:

private static async Task<T> Retry<T>(Func<T> func, int retryCount) 
    { 
     while (true) 
     { 
      try 
      { 
       var result = await Task.Run(func); 
       return result; 
      } 
      catch when (retryCount-- > 0){} 
     } 
    } 

và phiên bản đệ quy:

private static async Task<T> Retry<T>(Func<T> func, int retryCount) 
    { 
     try 
     { 
      var result = await Task.Run(func); 
      return result; 
     } 
     catch when (retryCount-- > 0){} 
     return await Retry(func, retryCount); 
    } 

ORIGINAL

Có nhiều cách để mã chức năng Thử lại: bạn có thể sử dụng tính năng đệ quy hoặc lặp lại tác vụ. Đã có một discussion trong nhóm Người dùng .NET của Hy Lạp trong khi quay lại các cách khác nhau để thực hiện chính xác điều này.
Nếu bạn đang sử dụng F #, bạn cũng có thể sử dụng cấu trúc Async. Thật không may, bạn không thể sử dụng các cấu trúc async/await ít nhất trong Async CTP, bởi vì mã được tạo ra bởi trình biên dịch không giống như nhiều chờ đợi hoặc có thể rethrows trong các khối catch.

Phiên bản đệ quy có lẽ là cách đơn giản nhất để xây dựng Thử lại trong C#.Các phiên bản sau không sử dụng Unwrap và cho biết thêm một sự chậm trễ bắt buộc trước khi thử lại:

private static Task<T> Retry<T>(Func<T> func, int retryCount, int delay, TaskCompletionSource<T> tcs = null) 
    { 
     if (tcs == null) 
      tcs = new TaskCompletionSource<T>(); 
     Task.Factory.StartNew(func).ContinueWith(_original => 
     { 
      if (_original.IsFaulted) 
      { 
       if (retryCount == 0) 
        tcs.SetException(_original.Exception.InnerExceptions); 
       else 
        Task.Factory.StartNewDelayed(delay).ContinueWith(t => 
        { 
         Retry(func, retryCount - 1, delay,tcs); 
        }); 
      } 
      else 
       tcs.SetResult(_original.Result); 
     }); 
     return tcs.Task; 
    } 

Chức năng StartNewDelayed xuất phát từ ParallelExtensionsExtras mẫu và sử dụng một bộ đếm thời gian để kích hoạt một TaskCompletionSource khi timeout xảy ra.

Chiếc F # phiên bản đơn giản hơn rất nhiều:

let retry (asyncComputation : Async<'T>) (retryCount : int) : Async<'T> = 
let rec retry' retryCount = 
    async { 
     try 
      let! result = asyncComputation 
      return result 
     with exn -> 
      if retryCount = 0 then 
       return raise exn 
      else 
       return! retry' (retryCount - 1) 
    } 
retry' retryCount 

Unfortunatley, nó là không thể để viết một cái gì đó tương tự như trong C# sử dụng async/chờ đợi từ Async CTP vì trình biên dịch không thích chờ đợi báo cáo bên trong một khối catch. Nỗ lực sau đây cũng không silenty, bởi vì thời gian chạy không thích gặp phải một chờ đợi sau một ngoại lệ:

private static async Task<T> Retry<T>(Func<T> func, int retryCount) 
    { 
     while (true) 
     { 
      try 
      { 
       var result = await TaskEx.Run(func); 
       return result; 
      } 
      catch 
      { 
       if (retryCount == 0) 
        throw; 
       retryCount--; 
      } 
     } 
    } 

Đối với yêu cầu người dùng, bạn có thể sửa đổi Thử lại để gọi một chức năng mà yêu cầu người dùng và trả về một nhiệm vụ thông qua một TaskCompletionSource để kích hoạt các bước tiếp theo khi các câu trả lời của người dùng, ví dụ:

private static Task<bool> AskUser() 
    { 
     var tcs = new TaskCompletionSource<bool>(); 
     Task.Factory.StartNew(() => 
     { 
      Console.WriteLine(@"Error Occured, continue? Y\N"); 
      var response = Console.ReadKey(); 
      tcs.SetResult(response.KeyChar=='y'); 

     }); 
     return tcs.Task; 
    } 

    private static Task<T> RetryAsk<T>(Func<T> func, int retryCount, TaskCompletionSource<T> tcs = null) 
    { 
     if (tcs == null) 
      tcs = new TaskCompletionSource<T>(); 
     Task.Factory.StartNew(func).ContinueWith(_original => 
     { 
      if (_original.IsFaulted) 
      { 
       if (retryCount == 0) 
        tcs.SetException(_original.Exception.InnerExceptions); 
       else 
        AskUser().ContinueWith(t => 
        { 
         if (t.Result) 
          RetryAsk(func, retryCount - 1, tcs); 
        }); 
      } 
      else 
       tcs.SetResult(_original.Result); 
     }); 
     return tcs.Task; 
    } 

với tất cả các continuations, bạn có thể thấy tại sao một phiên bản async của Thử lại rất đáng mơ ước.

UPDATE:

Trong Visual Studio 2012 Beta hai phiên bản sau làm việc:

Một phiên bản với một vòng lặp while:

private static async Task<T> Retry<T>(Func<T> func, int retryCount) 
    { 
     while (true) 
     { 
      try 
      { 
       var result = await Task.Run(func); 
       return result; 
      } 
      catch 
      { 
       if (retryCount == 0) 
        throw; 
       retryCount--; 
      } 
     } 
    } 

và một phiên bản đệ quy:

private static async Task<T> Retry<T>(Func<T> func, int retryCount) 
    { 
     try 
     { 
      var result = await Task.Run(func); 
      return result; 
     } 
     catch 
     { 
      if (retryCount == 0) 
       throw; 
     } 
     return await Retry(func, --retryCount); 
    } 
+0

Cảm ơn bạn đã giải thích chi tiết, hãy thử cách này và cho bạn biết –

+1

+1 Làm tốt lắm. [Đã thêm một đoạn dưới đây] (http://stackoverflow.com/a/16354355/11635) –

2

Khi ở mức cao, tôi thấy nó giúp tạo chữ ký chức năng từ những gì bạn có và những gì bạn muốn.

Bạn có:

  • Một chức năng cung cấp cho bạn một nhiệm vụ (Func<Task>). Chúng tôi sẽ sử dụng chức năng vì bản thân các tác vụ không thể thử lại được.
  • Một chức năng xác định nếu nhiệm vụ tổng thể được hoàn thành hoặc sẽ được thử lại (Func<Task, bool>)

Bạn muốn:

  • một nhiệm vụ chung

Vì vậy, bạn sẽ có một chức năng như:

Task Retry(Func<Task> action, Func<Task, bool> shouldRetry); 

Mở rộng thực hành bên trong hàm, nhiệm vụ khá nhiều có 2 thao tác để thực hiện với chúng, đọc trạng thái của chúng và ContinueWith. Để thực hiện các nhiệm vụ của riêng bạn, TaskCompletionSource là điểm khởi đầu tốt. Lần thử đầu tiên có thể trông giống như sau:

//error checking 
var result = new TaskCompletionSource<object>(); 
action().ContinueWith((t) => 
    { 
    if (shouldRetry(t)) 
     action(); 
    else 
    { 
     if (t.IsFaulted) 
      result.TrySetException(t.Exception); 
     //and similar for Canceled and RunToCompletion 
    } 
    }); 

Vấn đề rõ ràng ở đây là chỉ có 1 lần thử lại sẽ xảy ra. Để giải quyết vấn đề đó, bạn cần phải thực hiện một cách để hàm gọi chính nó. Cách thông thường để làm điều này với lambdas là một cái gì đó như thế này:

//error checking 
var result = new TaskCompletionSource<object>(); 

Func<Task, Task> retryRec = null; //declare, then assign 
retryRec = (t) => { if (shouldRetry(t)) 
         return action().ContinueWith(retryRec).Unwrap(); 
        else 
        { 
         if (t.IsFaulted) 
          result.TrySetException(t.Exception); 
         //and so on 
         return result.Task; //need to return something 
        } 
        }; 
action().ContinueWith(retryRec); 
return result.Task; 
+0

Cảm ơn, sẽ hãy thử và cho bạn biết –

4

Đây là phiên bản được riffed của Panagiotis Kanavos's excellent answer mà tôi đã thử nghiệm và đang sử dụng trong sản xuất.

Nó đề cập đến một số điều mà rất quan trọng với tôi:

  • muốn để có thể quyết định có nên thử lại dựa trên số trước nỗ lực và ngoại lệ từ nỗ lực hiện tại
  • Không muốn dựa vào async (hạn chế ít môi trường)
  • Muốn có kết quả Exception trong trường hợp thất bại bao gồm chi tiết từ mỗi nỗ lực


static Task<T> RetryWhile<T>(
    Func<int, Task<T>> func, 
    Func<Exception, int, bool> shouldRetry) 
{ 
    return RetryWhile<T>(func, shouldRetry, new TaskCompletionSource<T>(), 0, Enumerable.Empty<Exception>()); 
} 

static Task<T> RetryWhile<T>( 
    Func<int, Task<T>> func, 
    Func<Exception, int, bool> shouldRetry, 
    TaskCompletionSource<T> tcs, 
    int previousAttempts, IEnumerable<Exception> previousExceptions) 
{ 
    func(previousAttempts).ContinueWith(antecedent => 
    { 
     if (antecedent.IsFaulted) 
     { 
      var antecedentException = antecedent.Exception; 
      var allSoFar = previousExceptions 
       .Concat(antecedentException.Flatten().InnerExceptions); 
      if (shouldRetry(antecedentException, previousAttempts)) 
       RetryWhile(func,shouldRetry,previousAttempts+1, tcs, allSoFar); 
      else 
       tcs.SetException(allLoggedExceptions); 
     } 
     else 
      tcs.SetResult(antecedent.Result); 
    }, TaskContinuationOptions.ExecuteSynchronously); 
    return tcs.Task; 
} 
Các vấn đề liên quan