The ApmAsyncFactory interop hangs if a path in async method returns synchronously.
fiseni opened this issue · 1 comments
Hi,
I observed a strange behavior in ApmAsyncFactory. If the task contains a path that returns synchronously, then the execution hangs. It's easy to reproduce, so I'll just provide an example. I'll use asmx web service to demonstrate the issue.
public class DemoService : System.Web.Services.WebService
{
[WebMethod]
public IAsyncResult BeginRun(int option, AsyncCallback callback, object state)
{
return ApmAsyncFactory.ToBegin<string>(GetResponseAsync(option), callback, state);
}
[WebMethod]
public string EndRun(IAsyncResult result)
{
return ApmAsyncFactory.ToEnd<string>(result);
}
private async Task<string> GetResponseAsync(int option)
{
if (option == 1)
{
await Task.Delay(1000);
return "Works";
}
else if (option == 2)
{
// I tried awaiting a completed task too, just to test it out
await Task.CompletedTask;
return "Hangs";
}
else
{
return "Hangs";
}
}
}
To be sure that this is not a generic problem, I tried the solution provided in this article, and it works without issues.
public class DemoService : System.Web.Services.WebService
{
[WebMethod]
public IAsyncResult BeginRun(int option, AsyncCallback callback, object state)
{
return GetResponseAsync(option).AsApm(callback, state);
}
[WebMethod]
public string EndRun(IAsyncResult result)
{
return ((Task<string>)result).Result;
}
private async Task<string> GetResponseAsync(int option)
{
if (option == 1)
{
await Task.Delay(1000);
return "Works";
}
else if (option == 2)
{
await Task.CompletedTask;
return "Works";
}
else
{
return "Works";
}
}
}
It seems there is a race condition. If there is an asynchronous execution, the ToBegin
returns before the callback is invoked. On the other hand, if the task is already completed, the callback is invoked before the ToBegin
manages to return a result.
Just to confirm this, I tried adding a small delay before the callback is invoked, and it works properly then.
EDIT: There is no even race condition here. If the task is already completed, CompleteAsync
will run synchronously, and a callback always will be invoked before ToBegin
returns;