Majesty Cherry Tomato
Majesty Cherry Tomato

Reputation: 181

HttpClient SendAsync DeadLock

I have a wrapper that creates a function public async Task getCacheToken created for a few internal services/Application to call

and I'm experiencing this exception (please see the following) by calling performExtract() in another service.

performExtract is literally calling getCacheToken via API call

I can't help to send an async call inside the sync method (legacy environment), so every time when I call var results = client.SendAsync(requestData).Result' in a loop it will cause deadlocks, if I understand it correctly, sendAsync inside for loop, it will wait for a task to be finished before starting another one, so it should not have an exception of the following (connection disposed?)

to fix it, I have to override send Async ConfigureAwait(false) and it resolved my problem.

my question is how adding ConfigureAwait(false) solve the problem?

To avoid this issue, you can use a method called ConfigureAwait with a false parameter. When you do, this tells the Task that it can resume itself on any thread that is available instead of waiting for the thread that originally created it. This will speed up responses and avoid many deadlocks.

and how running it async cause a deadlock?

Thanks so much for all your patient reading thru the post.

 protected override ExtractResultStatus PerformExtract()
        {
            //EngageRestClient client = new EngageRestClient(_apiEndPoint);
            //client.Authenticator = new NtlmAuthenticator();
            ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12;
            try
            {
                var numErrors = 0;

                var dt = GetInfos();
                var fileName = string.Format(FilenameBase, DateTime.Now);
                if (dt.Rows.Count > 0)
                {
                    dt.Columns.Add("failureReason");
                    foreach (DataRow row in dt.Rows)
                    {
                        var referenceID = row["U3l_ReferenceId"].ToString();
                        var requestData = new HttpRequestMessage
                        {
                            Method = HttpMethod.Get,
                            RequestUri = new Uri(_apiEndPoint + $"?referenceID={referenceID}"),
                        };

                        requestData.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _token);
                        var results = client.SendAsync(requestData).Result;
                        var resultResponse = results.Content.ReadAsStringAsync().Result;








    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.StatusCode != HttpStatusCode.InternalServerError ||
                response.StatusCode != HttpStatusCode.NotImplemented ||
                response.StatusCode != HttpStatusCode.GatewayTimeout ||
                response.StatusCode != HttpStatusCode.ServiceUnavailable)
            {
                return response;
            }

            response.Dispose();
        }

        return response;
    }



public async Task<string> GetCacheToken()
        {
            ObjectCache cache = MemoryCache.Default;
            string refreshToken = cache.Get("refreshToken", null) == null ? GetToken() : cache.Get("refreshToken", null).ToString();

            if (!cache.Contains("apiToken"))
            {
                var httpContent = new StringContent("", Encoding.UTF8, "application/x-www-form-urlencoded");
                var dict = new Dictionary<string, string>();
                dict.Add("grant_type", "refresh_token");
                dict.Add("refresh_token", refreshToken);
                var requestData = new HttpRequestMessage
                {
                    Method = HttpMethod.Post,
                    RequestUri = new Uri("https://oauth2.sky.blackbaud.com/token"),
                    Content = new FormUrlEncodedContent(dict)
                };

                requestData.Headers.Authorization = new AuthenticationHeaderValue("Basic", Settings.BasicAuth);
                var results = await _client.SendAsync(requestData);
                var resultResponse = results.Content.ReadAsStringAsync().Result;

                try
                {
                    results.EnsureSuccessStatusCode();
                    var result = _js.Deserialize<TokenModel>(resultResponse);
                    //token expires in one hour from blackbaud
                    var expiration = DateTimeOffset.UtcNow.AddMinutes(55);
                    cache.Add("apiToken", result.access_token, expiration);
                    cache.Add("refreshToken", result.refresh_token, expiration);
                    UpdateToken(result.access_token, result.refresh_token);
                }
                catch (Exception e)
                {
                    var exceptionMessage = $"ResultMessage : {resultResponse} Exception: {e}. Message: {e.Message}. Stacktrace {e.StackTrace}";
                    Log.Exception(e,exceptionMessage);
                    throw;
                }
            }

            return cache.Get("apiToken", null).ToString();
        }

{Data: [], HResult: -2146233088, HelpLink: null, InnerException: null, Message: "Response status code does not indicate success: 400 (Bad Request).", Source: "System.Net.Http", StackTrace: " at System.Net.Http.HttpResponseMessage.EnsureSuccessStatusCode()\r\n at RaisersEdge.Infrastructure.Cache.d__2.MoveNext()\r\n--- End of stack trace from previous location where exception was thrown ---\r\n at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()\r\n
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\r\n at RaisersEdge.Controllers.BaseController.d__6.MoveNext()\r\n--- End of stack trace from previous location where exception was thrown ---\r\n at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()\r\n
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\r\n at System.Threading.Tasks.TaskHelpersExtensions.d__3`1.MoveNext()\r\n--- End of stack trace from previous location where exception was thrown ---\r\n at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()\r\n
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\r\n at System.Web.Http.Controllers.ApiControllerActionInvoker.d__0.MoveNext()\r\n--- End of stack trace from previous location where exception was thrown ---\r\n at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()\r\n
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\r\n at System.Web.Http.Controllers.ActionFilterResult.d__2.MoveNext()\r\n--- End of stack trace from previous location where exception was thrown ---\r\n at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()\r\n
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\r\n at System.Web.Http.Dispatcher.HttpControllerDispatcher.d__1.MoveNext()", TargetSite: "System.Net.Http.HttpResponseMessage EnsureSuccessStatusCode()", _typeTag: "HttpRequestException"}

Upvotes: 0

Views: 2860

Answers (2)

Stephen Cleary
Stephen Cleary

Reputation: 456777

how running it async cause a deadlock?

await by default captures a context and resumes executing the async method in that context. If this context only allows one thread at a time, and the calling code blocks a thread in that context by calling Result or Wait, then the code causes a deadlock since the async method cannot resume (and thus cannot complete).

how adding ConfigureAwait(false) solve the problem?

Because the await no longer captures its context. The async method can resume on any thread pool thread, and isn't affected by the thread blocked in the context. ConfigureAwait(false) is generally considered a best practice for library code.

one of the service is in a legacy environment cannot call await for because I don't have an async function to override with

There are a variety of hacks you can use to attempt to block on asynchronous code safely. None of them work in every situation. If ConfigureAwait(false) works for you, then I would use that.

Upvotes: 2

Ali Bahrami
Ali Bahrami

Reputation: 6073

As other folks mentioned you SHOULD not use .Result because it's evil! but in your case that you are working with a legacy app you can use this workaround:

using System.Threading.Tasks;

public class AsyncHelper
{
        private static readonly TaskFactory _taskFactory = new
            TaskFactory(CancellationToken.None,
                        TaskCreationOptions.None,
                        TaskContinuationOptions.None,
                        TaskScheduler.Default);

        public static TReturn RunSync<TReturn>(Func<Task<TReturn>> task)
        {
            return  _taskFactory.StartNew(task)
                                .Unwrap()
                                .GetAwaiter()
                                .GetResult();
        }
}

Then you can easily call your method with the helper:

var results = AsyncHelper.RunSync<System.Net.Http.HttpResponseMessage>( 
 () => client.SendAsync(requestData)
);

The helper class creates, configure and runs an async task, then unwraps it and waits for it synchronously to get the results: This is almost what await does, this approach will prevent deadlocks and could be used within a try/catch block.

Of course, the only correct way to calling async method is using await but this workaround is a better option when you have no way to call an async method inside sync method.

Upvotes: 3

Related Questions