2013-07-22 19 views
5

Budujemy wysoce współbieżną aplikację internetową, a ostatnio zaczęliśmy intensywnie korzystać z programowania asynchronicznego (przy użyciu TPL i async/await).HttpContent.ReadAsStringAsync powoduje żądanie zawieszenia (lub inne dziwne zachowania).

Mamy rozproszone środowisko, w którym aplikacje komunikują się ze sobą za pośrednictwem interfejsów REST API (wbudowanych w ASP.NET Web API). W jednej konkretnej aplikacji mamy DelegatingHandler, która po wywołaniu base.SendAsync (tj. Po obliczeniu odpowiedzi) rejestruje odpowiedź na plik. Mamy zawierać podstawowe informacje na odpowiedź w dzienniku (kod stanu, nagłówki i treść):

public static string SerializeResponse(HttpResponseMessage response) 
{ 
    var builder = new StringBuilder(); 
    var content = ReadContentAsString(response.Content); 

    builder.AppendFormat("HTTP/{0} {1:d} {1}", response.Version.ToString(2), response.StatusCode); 
    builder.AppendLine(); 
    builder.Append(response.Headers); 

    if (!string.IsNullOrWhiteSpace(content)) 
    { 
     builder.Append(response.Content.Headers); 

     builder.AppendLine(); 
     builder.AppendLine(Beautified(content)); 
    } 

    return builder.ToString(); 
} 

private static string ReadContentAsString(HttpContent content) 
{ 
    return content == null ? null : content.ReadAsStringAsync().Result; 
} 

Problem jest taki: gdy kod osiągnie content.ReadAsStringAsync().Result pod dużym obciążeniem serwera, żądanie czasami zawiesza się na IIS. Kiedy to robi, czasami zwraca odpowiedź - ale zawiesza się na IIS tak, jakby tego nie robił - lub w innych przypadkach nigdy nie zwraca.

Próbowałem również przeczytać zawartość przy użyciu ReadAsByteArrayAsync, a następnie przekonwertować ją na String, bez powodzenia.

Kiedy konwertować kod do wykorzystania asynchroniczny całym uzyskać rezultaty nawet dziwacznych w:

public static async Task<string> SerializeResponseAsync(HttpResponseMessage response) 
{ 
    var builder = new StringBuilder(); 
    var content = await ReadContentAsStringAsync(response.Content); 

    builder.AppendFormat("HTTP/{0} {1:d} {1}", response.Version.ToString(2), response.StatusCode); 
    builder.AppendLine(); 
    builder.Append(response.Headers); 

    if (!string.IsNullOrWhiteSpace(content)) 
    { 
     builder.Append(response.Content.Headers); 

     builder.AppendLine(); 
     builder.AppendLine(Beautified(content)); 
    } 

    return builder.ToString(); 
} 

private static Task<string> ReadContentAsStringAsync(HttpContent content) 
{ 
    return content == null ? Task.FromResult<string>(null) : content.ReadAsStringAsync(); 
} 

Teraz HttpContext.Current jest null po wywołaniu content.ReadAsStringAsync() i utrzymuje będąc null dla wszystkich kolejnych żądań! Wiem, że to brzmi niewiarygodnie - zajęło mi to trochę czasu i obecność trzech współpracowników, aby zaakceptować fakt, że tak naprawdę się dzieje.

Czy jest to oczekiwane zachowanie? Czy robię coś złego tutaj?

+2

Zdajesz sobie sprawę, że wywołanie 'ReadContentAsStringAsync' a następnie natychmiast dzwoni' Result' na to jest w zasadzie negując asynchrony, dobrze? To zablokuje, dopóki zadanie nie zostanie zakończone. A 'HttpContext.Current' mający' null' po oczekiwaniu brzmi, jakby po prostu nie płynął przez 'oczekujące' punkty, co jest irytujące, ale nie * całkowicie * zaskakuje mnie. Możesz go pobrać na * początku * metody asynchronicznej, a następnie użyć tej zmiennej lokalnej ... –

+0

Prawdopodobnie nazwałbym wrap próbą złapania wokół wywołania response.EnsureSuccessStatusCode() przed próbą przetworzenia zawartości. – cgotberg

+2

Czy jesteś pewien, że zawsze można przeczytać 'HttpResponseMessage.Content'? Wydaje mi się, że próba odczytu twojego własnego strumienia wyjściowego może nie być obsługiwana. –

Odpowiedz

0

Co do drugiego wydania, async/await to cukier syntaktyczny dla kompilatora budującego maszynę stanów, w której wywołanie funkcji poprzedzonej przez "czekaj" natychmiast zwraca bieżący wątek ... zawierający HttpContext . Bieżące przechowywanie w jego wątku. Zakończenie tego asynchronicznego wywołania może wystąpić na innym wątku o innej nazwie ... takiej, która NIE ma HttpContext.Current w swojej lokalnej pamięci wątku.

Jeśli chcesz, aby zakończenie zostało wykonane na tym samym wątku (tym samym mając te same obiekty w lokalnym magazynie wątków, takim jak HttpContext.Current), musisz mieć świadomość tego zachowania. Jest to szczególnie ważne w przypadku wywołań z głównego wątku UI (jeśli tworzysz aplikację Windows) lub w ASP.NET, wywołania z wątku żądania ASP.NET, gdzie jesteś zależny od HttpContext.Current.

Zobacz dokumentację referencyjną na temat programu ConfigureAwait (wartość false). Zobacz także niektóre samouczki z kanału 9 na licencji TPL. Gdy tylko "łatwe" rzeczy zostaną pomieszane, prezenter będzie zawsze mówił o tym problemie, ponieważ powoduje subtelne problemy, które nie są łatwe do zrozumienia, chyba że wiesz, co robi TPL pod okładkami.

Powodzenia.

Jeśli chodzi o pierwszy problem, jeśli dzwoniący uzyskuje wynik, nie jestem przekonany, że usługi IIS nie zakończyły tego żądania. W jaki sposób określasz, że wątek żądania ASP.NET zainicjowany przez tego wywołującego jest zawieszony w IIS?

5

Miałem ten problem.Mimo, że nie w pełni jeszcze przetestowany, używając CopyToAsync zamiast ReadAsStringAsync wydaje się rozwiązać problem:

var ms = new MemoryStream(); 
await response.Content.CopyToAsync(ms); 
ms.Seek(0, SeekOrigin.Begin); 

var sr = new StreamReader(ms); 
responseContent = sr.ReadToEnd(); 
+0

to działa również dla mnie! – Calvin