2017-12-26 15 views
2

.NET Framework 4.6.1を使用しています。非同期メソッドで使用するとHttpClientヘッダーがゼロになる

私はすべてのhttp要求を処理する静的なHttpClientを持っている私のWeb APIにコントローラを持っています。私はおよそ月に一度では、IIS上で自分のアプリケーションをホストされた後、私は私のアプリへのすべての着信要求に対して次の例外を取得:

System.ArgumentNullException: Value cannot be null. 
    at System.Threading.Monitor.Enter(Object obj) 
    at System.Net.Http.Headers.HttpHeaders.ParseRawHeaderValues(String name, HeaderStoreItemInfo info, Boolean removeEmptyHeader) 
    at System.Net.Http.Headers.HttpHeaders.AddHeaders(HttpHeaders sourceHeaders) 
    at System.Net.Http.Headers.HttpRequestHeaders.AddHeaders(HttpHeaders sourceHeaders) 
    at System.Net.Http.HttpClient.PrepareRequestMessage(HttpRequestMessage request) 
    at System.Net.Http.HttpClient.SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken) 
    at System.Net.Http.HttpClient.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 
    at System.Net.Http.HttpClient.PutAsync(Uri requestUri, HttpContent content, CancellationToken cancellationToken) 
    at Attributes.Controllers.AttributesBaseController.<UpdateAttributes>d__6.MoveNext() in D:\Git\PortalSystem\Attributes\Controllers\AttributesBaseController.cs:line 42 

私はIIS上のアプリケーションプールを再起動すると、すべてが再び正常に動作を開始し。私が持っているコードはここにあります:

public class AttributesBaseController : ApiController 
{ 
    [Inject] 
    public IPortalsRepository PortalsRepository { get; set; } 

    private static HttpClient Client = new HttpClient(new HttpClientHandler { Proxy = null, UseProxy = false }) 
                      { Timeout = TimeSpan.FromSeconds(double.Parse(WebConfigurationManager.AppSettings["httpTimeout"])) }; 
    private static readonly Logger logger = LogManager.GetCurrentClassLogger(); 

    protected async Task UpdateAttributes(int clientId, int? updateAttrId = null) 
    { 
     try 
     { 
      Client.DefaultRequestHeaders.Accept.Clear(); 
      Client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 

      #region Update Client Dossier !!! BELOW IS LINE 42 !!!!   
      using (var response = await Client.PutAsync(new Uri(WebConfigurationManager.AppSettings["dossier"] + "api/dossier?clientId=" + clientId), null)) 
      { 
       if (!response.IsSuccessStatusCode) 
       { 
        logger.Error($"Dossier update failed"); 
       } 
      } 
      #endregion 

      #region Gather Initial Info 
      var checkSystems = PortalsRepository.GetCheckSystems(clientId); 
      var currentAttributes = PortalsRepository.GetCurrentAttributes(clientId, checkSystems); 
      #endregion 

      List<Task> tasks = new List<Task>(); 
      #region Initialize Tasks 
      foreach (var cs in checkSystems) 
      { 
       if (!string.IsNullOrEmpty(cs.KeyValue)) 
       { 
        tasks.Add(Task.Run(async() => 
        { 
          var passedAttributes = currentAttributes.Where(ca => ca.SystemId == cs.SystemId && ca.AttributeId == cs.AttributeId && 
          (ca.SysClientId == cs.KeyValue || ca.OwnerSysClientId == cs.KeyValue)).ToList(); 

          if (cs.AttributeId == 2 && (updateAttrId == null || updateAttrId == 2)) 
          { 
           await UpdateOpenWayIndividualCardsInfo(passedAttributes, cs, clientId); 
          } 
          else if (cs.AttributeId == 3 && (updateAttrId == null || updateAttrId == 3)) 
          { 
           await UpdateEquationAccountsInfo(passedAttributes, cs, clientId); 
          } 
          else if (cs.AttributeId == 8 && (updateAttrId == null || updateAttrId == 8)) 
          { 
           await UpdateOpenWayCorporateInfo(passedAttributes, cs, clientId); 
          } 
          else if (cs.AttributeId == 9 && (updateAttrId == null || updateAttrId == 9)) 
          { 
           await UpdateEquationDealsInfo(passedAttributes, cs, clientId); 
          } 
          else if (cs.AttributeId == 10 && (updateAttrId == null || updateAttrId == 10)) 
          { 
           await UpdateOpenWayIndividualCardDepositsInfo(passedAttributes, cs, clientId); 
          } 
          else if (cs.AttributeId == 16 && (updateAttrId == null || updateAttrId == 16)) 
          { 
           await UpdateOpenWayBonusInfo(passedAttributes, cs, clientId); 
          } 
          else if (cs.AttributeId == 17 && (/*updateAttrId == null ||*/ updateAttrId == 17)) 
          { 
           await UpdateExternalCardsInfo(passedAttributes, cs, clientId); 
          } 
          if (cs.AttributeId == 18 && (updateAttrId == null || updateAttrId == 18)) 
          { 
           await UpdateCRSInfo(passedAttributes, cs, clientId); 
          } 
          else if (cs.AttributeId == 22 && (updateAttrId == null || updateAttrId == 22)) 
          { 
           await UpdateCardInsuranceInfo(passedAttributes, cs, clientId); 
          } 
        })); 
       } 
      } 
      #endregion 

      // Run all tasks 
      await Task.WhenAny(Task.WhenAll(tasks.ToArray()), Task.Delay(TimeSpan.FromSeconds(double.Parse(WebConfigurationManager.AppSettings["taskWaitTime"])))); 
     } 
     catch (Exception ex) 
     { 
      logger.Error(ex); 
     } 
    } 
} 

誰かが私に助言/助けを借りて問題を理解できますか?私は、問題が私がタスクでHttpClientを使用しているか、IISで何か悪いことが起こっているかどうかは分かりません。

+0

_ "IISで何か悪いことが起こる" _ - 完全に不可能ではありませんが、_extremely_はありそうもありません。しかし、問題を確実に再現する良い[mcve]がなければ、スタックオーバーフローコミュニティは良い、具体的な答えの方法で提供できるものはありません。 –

+0

@PeterDuniho要求を手動で行うと、問題を再現できないという問題があります。多分あなたは私に問題の調査を開始するための助言を与えることができますか? – Nomad

+2

最初のステップは他の問題と同じです。できるだけシナリオを単純化してください。あなたは1ヶ月に1回しか起こらないと言いますから、しばらく時間がかかるかもしれません。別の標準的な手法は、ログを追加することです。この場合、それはあなたの最善の策かもしれません。問題の原因となっているコードの一般的な領域を少なくともいくつか考えている場合は、すべての状態を記録するログを追加して、問題の再現にどのような状態が必要かを知ることができます。 ... –

答えて

4

DefaultRequestHeadersの実装を見てみると、我々はそれがヘッダを保存するために、簡単な辞書を使用していることを見ることができます:

private Dictionary<string, HttpHeaders.HeaderStoreItemInfo> headerStore; 

DefaultRequestHeaders.Accept.Clearはちょうど同期のいずれかの種類せずに、辞書からキーを削除します

public bool Remove(string name) 
{ 
    this.CheckHeaderName(name); 
    if (this.headerStore == null) 
    return false; 
    return this.headerStore.Remove(name); 
} 

Dictionary.Removeはスレッドセーフではありません。この操作中に辞書にアクセスすると、予期しない動作が発生する可能性があります。私たちはあなたのスタックトレースでParseRawHeaderValues方法を見れば

は今:

private bool ParseRawHeaderValues(string name, HttpHeaders.HeaderStoreItemInfo info, bool removeEmptyHeader) 
{ 
    lock (info) 
    { 
    // stuff 
    } 
    return true; 
} 

我々はNULLにinfoによってエラーが原因だろうと見ることができます。今、発信者を見て:

internal virtual void AddHeaders(HttpHeaders sourceHeaders) 
{ 
    if (sourceHeaders.headerStore == null) 
    return; 
    List<string> stringList = (List<string>) null; 
    foreach (KeyValuePair<string, HttpHeaders.HeaderStoreItemInfo> keyValuePair in sourceHeaders.headerStore) 
    { 
    if (this.headerStore == null || !this.headerStore.ContainsKey(keyValuePair.Key)) 
    { 
     HttpHeaders.HeaderStoreItemInfo headerStoreItemInfo = keyValuePair.Value; 
     if (!sourceHeaders.ParseRawHeaderValues(keyValuePair.Key, headerStoreItemInfo, false)) 
     { 
     if (stringList == null) 
      stringList = new List<string>(); 
     stringList.Add(keyValuePair.Key); 
     } 
     else 
     this.AddHeaderInfo(keyValuePair.Key, headerStoreItemInfo); 
    } 
    } 
    if (stringList == null) 
    return; 
    foreach (string key in stringList) 
    sourceHeaders.headerStore.Remove(key); 
} 

かいつまんで、私たちはDefaultRequestHeadersで辞書を反復処理する(つまり、sourceHeaders.headerStoreだ)と、要求にヘッダをコピーします。

これを要約すると、同時に辞書の内容を繰り返すスレッドと、要素を追加/削除するスレッドがあります。これはあなたが見ている動作につながる可能性があります。それを変更することはありません、静的コンストラクタで

  1. 初期化DefaultRequestHeaders:カスタムヘッダの代わりPutAsync

    static AttributesBaseController 
    { 
        Client = new HttpClient(new HttpClientHandler { Proxy = null, UseProxy = false }) 
        { 
         Timeout = TimeSpan.FromSeconds(double.Parse(WebConfigurationManager.AppSettings["httpTimeout"])) 
        }; 
    
        Client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 
    } 
    
  2. 使用SendAsync

    は、次の2つのソリューションを持って、この問題を解決するには:

    var message = new HttpRequestMessage(HttpMethod.Put, new Uri(WebConfigurationManager.AppSettings["dossier"] + "api/dossier?clientId=" + clientId)); 
    message.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 
    using (var response = await Client.SendAsync(message)) 
    { 
        // ... 
    } 
    
ちょうど楽しみのため

、小さなREPRO:

var client = new HttpClient(); 

client.DefaultRequestHeaders.Accept.Clear(); 
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 

var storeField = typeof(HttpHeaders).GetField("headerStore", BindingFlags.Instance | BindingFlags.NonPublic); 

FieldInfo valueField = null; 

var store = (IEnumerable)storeField.GetValue(client.DefaultRequestHeaders); 

foreach (var item in store) 
{ 
    valueField = item.GetType().GetField("value", BindingFlags.Instance | BindingFlags.NonPublic); 

    Console.WriteLine(valueField.GetValue(item)); 
} 

for (int i = 0; i < 8; i++) 
{ 
    Task.Run(() => 
    { 
     int iteration = 0; 

     while (true) 
     { 
      iteration++; 

      try 
      { 
       foreach (var item in store) 
       { 
        var value = valueField.GetValue(item); 

        if (value == null) 
        { 
         Console.WriteLine("Iteration {0}, value is null", iteration); 
        } 

        break; 
       } 

       client.DefaultRequestHeaders.Accept.Clear(); 
       client.DefaultRequestHeaders.Accept.Add(new Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json")); 
      } 
      catch (Exception) { } 
     } 
    }); 
} 

Console.ReadLine(); 

出力:

System.Net.Http.Headers。HttpHeaders + HeaderStoreItemInfoは

反復137、値は、スレッドが同時に辞書にアクセスするとき、それはあなたのウェブサーバ上で発生した場合(無限ループにはまりする傾向があるため、いくつかの試行がかかることがあり、問題を再現

nullですASP.NETは、タイムアウトが経過した後にスレッドを中止します)。

+1

@Nomad - Kevinの答えのオプション2する。リクエストごとに 'DefaultRequestHeaders'を変更するべきではありません。彼らは彼らがスズについて言うように意図されています。つまり、 'HttpClient'のインスタンスが送信するすべてのメッセージに適用される** defaults **です。スレッド間で競合条件を作成しているだけです。要求ごとのヘッダーは 'HttpRequestMessage'に追加され、' HttpClient'には追加されません。 – sellotape

+0

詳細な説明ありがとうございます。私は自分のコードを変更し、サーバー上のアプリケーションを更新しました。今、私はそれがどう動くかを監視します。 – Nomad

関連する問題