web-dev-qa-db-ja.com

HttpClientの失敗したリクエストの再試行

HttpContentオブジェクトを指定して、要求を発行し、失敗すると再試行する関数を作成しています。ただし、HttpContentオブジェクトはリクエストの発行後に破棄されるという例外が発生します。とにかくHttpContentオブジェクトをコピーまたは複製して、複数の要求を発行できるようにします。

 public HttpResponseMessage ExecuteWithRetry(string url, HttpContent content)
 {
  HttpResponseMessage result = null;
  bool success = false;
  do
  {
      using (var client = new HttpClient())
      {
          result = client.PostAsync(url, content).Result;
          success = result.IsSuccessStatusCode;
      }
  }
  while (!success);

 return result;
} 

// Works with no exception if first request is successful
ExecuteWithRetry("http://www.requestb.in/xfxcva" /*valid url*/, new StringContent("Hello World"));
// Throws if request has to be retried ...
ExecuteWithRetry("http://www.requestb.in/badurl" /*invalid url*/, new StringContent("Hello World"));

(明らかに、私は無期限に試していませんが、上記のコードは基本的に私が欲しいものです)。

この例外が発生します

System.AggregateException: One or more errors occurred. ---> System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'System.Net.Http.StringContent'.
   at System.Net.Http.HttpContent.CheckDisposed()
   at System.Net.Http.HttpContent.CopyToAsync(Stream stream, TransportContext context)
   at System.Net.Http.HttpClientHandler.GetRequestStreamCallback(IAsyncResult ar)
   --- End of inner exception stack trace ---
   at System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions)
   at System.Threading.Tasks.Task`1.GetResultCore(Boolean waitCompletionNotification)
   at System.Threading.Tasks.Task`1.get_Result()
   at Submission#8.ExecuteWithRetry(String url, HttpContent content)

とにかくHttpContentオブジェクトを複製するか、再利用する方法はありますか?

38
samirahmed

HttpClientをラップする再試行機能を実装する代わりに、内部で再試行ロジックを実行するHttpClientHttpMessageHandlerを構築することを検討してください。例えば:

public class RetryHandler : DelegatingHandler
{
    // Strongly consider limiting the number of retries - "retry forever" is
    // probably not the most user friendly way you could respond to "the
    // network cable got pulled out."
    private const int MaxRetries = 3;

    public RetryHandler(HttpMessageHandler innerHandler)
        : base(innerHandler)
    { }

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        HttpResponseMessage response = null;
        for (int i = 0; i < MaxRetries; i++)
        {
            response = await base.SendAsync(request, cancellationToken);
            if (response.IsSuccessStatusCode) {
                return response;
            }
        }

        return response;
    }
}

public class BusinessLogic
{
    public void FetchSomeThingsSynchronously()
    {
        // ...

        // Consider abstracting this construction to a factory or IoC container
        using (var client = new HttpClient(new RetryHandler(new HttpClientHandler())))
        {
            myResult = client.PostAsync(yourUri, yourHttpContent).Result;
        }

        // ...
    }
}
69
Dan Bjorge

ASP.NET Core 2.1アンサー

ASP.NET Core 2.1がサポートを追加 for Polly 直接。ここで、UnreliableEndpointCallerServiceは、コンストラクターでHttpClientを受け入れるクラスです。失敗したリクエストは指数関数的なバックオフで再試行されるため、次の再試行は前のリクエストよりも指数関数的に長い時間で実行されます。

services
    .AddHttpClient<UnreliableEndpointCallerService>()
    .AddTransientHttpErrorPolicy(
        x => x.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(3, retryAttempt)));

また、私のブログ投稿を読むことを検討してください "HttpClientFactoryを最適に構成する"

他のプラットフォームの回答

この実装では Polly を使用して指数バックオフで再試行するため、次の再試行は前の再試行から指数関数的に長い時間で行われます。また、タイムアウトによりHttpRequestExceptionまたはTaskCanceledExceptionがスローされた場合も再試行します。 PollyはTopazよりもはるかに使いやすいです。

public class HttpRetryMessageHandler : DelegatingHandler
{
    public HttpRetryMessageHandler(HttpClientHandler handler) : base(handler) {}

    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken) =>
        Policy
            .Handle<HttpRequestException>()
            .Or<TaskCanceledException>()
            .OrResult<HttpResponseMessage>(x => !x.IsSuccessStatusCode)
            .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(3, retryAttempt)))
            .ExecuteAsync(() => base.SendAsync(request, cancellationToken));
}

using (var client = new HttpClient(new HttpRetryMessageHandler(new HttpClientHandler())))
{
    var result = await client.GetAsync("http://example.com");
}
43

現在の回答は、すべての場合、特にリクエストタイムアウトの非常に一般的なケースで期待どおりに機能しません(そこの私のコメントを参照)。

さらに、非常に単純な再試行戦略を実装します-指数バックオフ(Azure Storage Client APIのデフォルト)など、もう少し洗練されたものが必要な場合がよくあります。

私はつまずいた [〜#〜] topaz [〜#〜] を読みながら 関連するブログ投稿 (また、誤った内部再試行アプローチを提供している)。ここに私が思いついたものがあります:

// sample usage: var response = await RequestAsync(() => httpClient.GetAsync(url));
Task<HttpResponseMessage> RequestAsync(Func<Task<HttpResponseMessage>> requester)
{
    var retryPolicy = new RetryPolicy(transientErrorDetectionStrategy, retryStrategy);
    //you can subscribe to the RetryPolicy.Retrying event here to be notified 
    //of retry attempts (e.g. for logging purposes)
    return retryPolicy.ExecuteAsync(async () =>
    {
        HttpResponseMessage response;
        try
        {
            response = await requester().ConfigureAwait(false);
        }
        catch (TaskCanceledException e) //HttpClient throws this on timeout
        {
            //we need to convert it to a different exception
            //otherwise ExecuteAsync will think we requested cancellation
            throw new HttpRequestException("Request timed out", e);
        }
        //assuming you treat an unsuccessful status code as an error
        //otherwise just return the respone here
        return response.EnsureSuccessStatusCode(); 
    });
}

requesterデリゲートパラメーターに注意してください。同じリクエストを複数回送信できないため、notHttpRequestMessageである必要があります。戦略に関しては、それはあなたのユースケースに依存します。たとえば、一時的なエラー検出戦略は次のように単純な場合があります。

private sealed class TransientErrorCatchAllStrategy : ITransientErrorDetectionStrategy
{
    public bool IsTransient(Exception ex)
    {
        return true;
    }
}

再試行戦略については、TOPAZには3つのオプションがあります。

  1. FixedInterval
  2. インクリメンタル
  3. 指数バックオフ

たとえば、Azureクライアントストレージライブラリがデフォルトで使用するものと同等のTOPAZを次に示します。

int retries = 3;
var minBackoff = TimeSpan.FromSeconds(3.0);
var maxBackoff = TimeSpan.FromSeconds(120.0);
var deltaBackoff= TimeSpan.FromSeconds(4.0);
var strategy = new ExponentialBackoff(retries, minBackoff, maxBackoff, deltaBackoff);

詳細については、 http://msdn.Microsoft.com/en-us/library/hh680901(v = pandp.50).aspx を参照してください

[〜#〜] edit [〜#〜]リクエストにHttpContentオブジェクトが含まれる場合、再生成する必要があることに注意してください毎回、HttpClientによっても破棄されます(アレクサンドル・ペピンを捕まえてくれてありがとう)。例えば ​​() => httpClient.PostAsync(url, new StringContent("foo")))

28
Ohad Schneider

StringContentを複製することは、おそらく最良のアイデアではありません。ただし、簡単な変更で問題を解決できます。関数を変更して、ループ内でStringContentオブジェクトを作成するだけです。

public HttpResponseMessage ExecuteWithRetry(string url, string contentString)
{
   HttpResponseMessage result = null;
   bool success = false;
   using (var client = new HttpClient())
   {
      do
      {
         result = client.PostAsync(url, new StringContent(contentString)).Result;
         success = result.IsSuccessStatusCode;
      }
      while (!success);
  }    

  return result;
} 

そしてそれを呼び出す

ExecuteWithRetry("http://www.requestb.in/xfxcva" /*valid url*/, "Hello World");
15
VladL

これにより、受け入れられた回答が構築されますが、再試行の量を渡す機能が追加され、さらに各リクエストに非ブロッキング遅延/待機時間を追加する機能が追加されます。また、try catchを使用して、例外が発生した後も再試行が継続されるようにします。そして最後に、BadRequestsの場合にループから抜け出すためのコードを追加しました。同じ悪いリクエストを何度も再送信したくありません。

public class HttpRetryHandler : DelegatingHandler
{
    private int MaxRetries;
    private int WaitTime;

    public HttpRetryHandler(HttpMessageHandler innerHandler, int maxRetries = 3, int waitSeconds = 0)
        : base(innerHandler)
    {
        MaxRetries = maxRetries;
        WaitTime = waitSeconds * 1000; 
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        HttpResponseMessage response = null;
        for (int i = 0; i < MaxRetries; i++)
        {
            try
            {
                response = await base.SendAsync(request, cancellationToken);
                if (response.IsSuccessStatusCode)
                {
                    return response;
                }
                else if(response.StatusCode == HttpStatusCode.BadRequest)
                {
                    // Don't reattempt a bad request
                    break; 
                }
            }
            catch
            {
                // Ignore Error As We Will Attempt Again
            }
            finally
            {
                response.Dispose(); 
            }

            if(WaitTime > 0)
            {
                await Task.Delay(WaitTime);
            }
        }

        return response;
    }
}

}

1
TroySteven

私はそれを試し、単体テストと統合テストを使用しながら作業しました。しかし、実際にREST URL。から呼び出したときにスタックしました。 この興味深い投稿 が見つかりました。

_response = await base.SendAsync(request, cancellationToken);
_

これを修正するには、最後に.ConfigureAwait(false)を追加します。

_response = await base.SendAsync(request, token).ConfigureAwait(false);
_

このようにリンクトークンパーツの作成も追加しました。

_var linkedToken = cancellationToken.CreateLinkedSource();
linkedToken.CancelAfter(new TimeSpan(0, 0, 5, 0));
var token = linkedToken.Token;

HttpResponseMessage response = null;
for (int i = 0; i < MaxRetries; i++)
{
    response = await base.SendAsync(request, token).ConfigureAwait(false);
    if (response.IsSuccessStatusCode)
    {
        return response;
    }
}

return response;
_
0
Pranav Patel

RestEase And Taskでは、多くの呼び出し(シングルトン)で再利用されたhttpClientで再試行すると、TaskCanceledExceptionが発生します。これを修正するには、失敗した応答をDispose()してから再試行する必要があります

public class RetryHandler : DelegatingHandler
{
    // Strongly consider limiting the number of retries - "retry forever" is
    // probably not the most user friendly way you could respond to "the
    // network cable got pulled out."
    private const int MaxRetries = 3;

    public RetryHandler(HttpMessageHandler innerHandler)
        : base(innerHandler)
    { }

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        HttpResponseMessage response = null;
        for (int i = 0; i < MaxRetries; i++)
        {
            response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
            if (response.IsSuccessStatusCode) {
                return response;
            }

            response.Dispose();
        }

        return response;
    }
}
0
Pascal Martin

私はほとんど同じ問題を抱えています。 リクエスト配信を保証するHttpWebRequestキューイングライブラリ クラッシュを回避するためのアプローチを更新しました(EDIT3を参照)が、メッセージ配信(またはメッセージが配信されなかった場合の再配信)を保証する一般的なメカニズムが必要です。

0