Async/ConfigureAwait(True) does not wait - WebService - vb.net

I have a webservice GetCard which calls a function CreateRaces. Inside the CreateRaces I am saving the new objects. This works fine without async. However, when I call the async function for task1 CommitAsync the method executes and returns a response without hitting the next break point. I have tried to work around with via Task.Run, ConfigureWait(true), and with
With HttpContext.Current
bulk.Setup etc...
End With
but nothing seems to work.
The bulk.Setup - CommitAsync calls a third party dll (SqlBulkTools) which returns Task.
It looks like it works on a different thread.
Any ideas how to configure it so I the function executes and I can hit the next break point which is sw.Stop()?
<HttpGet>
<AllowAnonymous>
Public Async Function GetCard(cardurl As String) As HttpResponseMessage
Try
Dim oRace As New RaceCard(cardurl)
oRace.DoWork()
Dim oRC As New RaceController
await oRC.CreateRaces(oRace.Races)
Return Request.CreateResponse(HttpStatusCode.OK, True.ToJson)
Catch ex As Exception
Return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, ex)
End Try
End Function
CreateRaces function
Public Async Function CreateRaces(ByVal t As List(Of Race)) As Threading.Tasks.Task(Of Boolean)
Try
'Proceed only if list has items
If t.Count > 0 Then
Dim bulk As New BulkOperations()
Dim sw As New Stopwatch()
Dim oCon As New SqlClient.SqlConnection(DotNetNuke.Common.Utilities.Config.GetConnectionString)
sw.Start()
Dim task1 = Await bulk.Setup(Of Race).ForCollection(t).WithTable("ArbiRace").AddAllColumns().BulkInsertOrUpdate().SetIdentityColumn(Function(i) i.RaceID).MatchTargetOn(Function(m) m.RaceName).CommitAsync(oCon).ConfigureAwait(True)
sw.Stop()
Dim swt = sw.ElapsedMilliseconds
End If
Return True
Catch ex As Exception
Dim s As String
s = ex.ToString
Return False
End Try
End Function

Asnyc calls can be handled in 2 ways:
Deliberately halting thread execution until one, several, or all tasks are completed.
Leaving the task to run while the current thread goes to do something else.
Based on your question, you are expecting #1, but your code currently does #2. Await Documentation:
An Await expression or statement does not block the thread on which it is executing. Instead, it causes the compiler to sign up the rest of the async method, after the Await expression, as a continuation on the awaited task. Control then returns to the caller of the async method. When the task completes, it invokes its continuation, and execution of the async method resumes where it left off.
In your case, once CreateRaces() reaches the Dim task1 line (which will eventually store the result, not the task), control returns to GetCard(). However, the CreateRaces() call here also uses an Await, so control attempts to go back up the call stack again. Except, there is nowhere left to go up, because you are at the original endpoint. This is why you seem to get a response too soon.
Since you are waiting for the task to finish to get the elapsed time, you want to wait for the tasks to finish before returning a response. This can be accomplished with task.Result, task.Wait(), or the shared method Task.WaitAll().
Example:
<HttpGet>
<AllowAnonymous>
Public Function GetCard(ByVal cardUrl As String) As HttpResponseMessage
Try
Dim oRace As New RaceCard(cardurl)
oRace.DoWork()
Dim oRC As New RaceController
Dim createRacesTask As Threading.Tasks.Task(Of Boolean) = oRC.CreateRaces(oRace.Races)
'Do other tasks here while we wait
Threading.Tasks.Task.WaitAll(createRacesTask, ...)
Return Request.CreateResponse(HttpStatusCode.OK, True.ToJson)
Catch ex As Exception
Return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, ex)
End Try
End Function

Related

Thrown exception stopping application despite being in a try block

I have an async function that makes a call to an API, but sometimes there is bad data and I want an exception to be thrown to stop other subsequent procedures from having to run. The Async procedure looks like this:
public async Function getInfo(url as string) as task(of string)
Dim htpRes As HttpResponseMessage = Await url.GetAsync().ConfigureAwait(False)
Dim result = htpRes.Content.ReadAsStringAsync.Result
If result = "" Then
Throw New Exception("API Failed")
Else
Return result
End If
End Function
That function is called by a procedure that looks like this:
sub hitAllAPIs(apiList As List(Of String))
For each i In apiList
Try
Dim info As String = getInfo(i)
doOtherStuffWithInfo(info)
Catch ex As Exception
logError
End Try
Next
End sub
The desired behavior is for the forloop in 'hitAllAPIs' to keep running even if an exception is thrown within 'getInfo'. Instead, what happens is that the exception gets hit and stops the code from running, whether I'm in Debug mode or Release mode. If I'm not there to babysit it and hit 'continue' then the forloop will just stop and the program won't run anymore. Once I hit 'continue', btw, the 'Catch' will work and the error will be logged.
The issue is that I need this to all happen automatically and that's not happening. I can't just eliminate the exception and check the function for a null value, since this is a very simplified version of my code and the function is actually called all over the place. I know that I can change my exception settings to simply skip over all exceptions like this, but this is happening even in Release mode to code that has been deployed. I can't imagine that my debugging exceptions should have an effect on code deployed in Release mode. In any case, I'm hoping someone can help me understand why this exception isn't being automatically handled by the try block.
Thanks!
It seems result = "" is an expected result not an exception. Using Try/Catch is rather heavy handed. Exception handling is for unexpected results. Get rid of the Throw in the Function and add and If in the For Each.
Public Async Function getInfo(url As String) As Task(Of String)
Dim htpRes As HttpResponseMessage = Await url.GetAsync().ConfigureAwait(False)
Dim result = htpRes.Content.ReadAsStringAsync.Result
Return result
End Function
Sub hitAllAPIs(apiList As List(Of String))
For Each i In apiList
Dim info As String = getInfo(i)
If info = "" Then
'Add an overload of logError that accepts a string
logError("API failed")
Else
doOtherStuffWithInfo(info)
End If
Next
End Sub

Exception handling with Async Task functions

I am beginning to learn about Async / Task functions for running cancellable SQL queries in VB.NET.
I have the following code in a class library which runs two tasks and does not have any exception handling, as I would like to handle this in the application that calls the class library. I have the following code in the class library.
Public Async Function DirectoryList(ct As CancellationToken) As Task(Of List(Of Directory))
ct.ThrowIfCancellationRequested()
Dim ds As DataSet
Dim dirs As List(Of Directory)
Dim ctUnregister As CancellationTokenRegistration
ds = Await Task.Run(Function()
Using newConnection = New SqlConnection(Me.InitializationData.ConnectionString)
Using sqlAdapter = New SqlDataAdapter("DirectoryList", newConnection)
ctUnregister = ct.Register(Sub() sqlAdapter.SelectCommand.Cancel())
With sqlAdapter
.SelectCommand.CommandType = CommandType.StoredProcedure
.SelectCommand.CommandTimeout = Me.InitializationData.CommandTimeout
End With
Dim newDataSet As DataSet = New DataSet()
sqlAdapter.Fill(newDataSet)
Return newDataSet
End Using
End Using
' Remove the registration we set earlier so the cancellation token can be used normally.
ctUnregister.Dispose()
End Function, ct)
dirs = Await Task.Run(Function()
Dim dirsResult As New List(Of Directory)
Dim tbl As DataTable = ds.Tables(0)
For Each row As DataRow In tbl.Select()
' process the data
ct.ThrowIfCancellationRequested()
Next
Return dirsResult
End Function, ct)
Return dirs
End Function
I then call it as follows:
Try
dirs = Await databaseLibrary.DirectoryList(cts.Token)
MsgBox("Query complete!")
Catch ex As System.Data.SqlClient.SqlException
MsgBox("Cancelled (SQL)")
Catch ex2 As OperationCanceledException
MsgBox("Cancelled")
Catch ex3 As Exception
MsgBox("Cancelled")
End Try
Functionally it seems to work as expected - I can cancel the requests and exceptions are thrown as expected.
However I would like to handle the exceptions so I can gracefully cancel the tasks, but even when running in Debug mode within the IDE, the app still breaks and the exception (e.g. SqlException) is shown within the IDE. If I step on, eventually the Catch logic runs.
When the application is run outside of the IDE, the exception handling works as expected.
This seems different to the normal behaviour when running in the debugger- normally the exception handling runs but only breaks if the exception is unhandled.
Why is this behaviour different, presumably because of the Async functions?
when running in Debug mode within the IDE, the app still breaks and the exception (e.g. SqlException) is shown within the IDE.
Why is this behaviour different, presumably because of the Async functions?
Exceptions thrown by asynchronous functions are not caught directly by the catch. What actually happens is that the exception is caught by a compiler-generated state machine, and that exception is placed on the returned Task. Later, when that task is awaited, the exception is re-raised, and then can be caught by the catch.
However, this "indirection" in exception handling means that the IDE does kind of freak out when the exception is initially thrown. As far as it can tell, there is no catch in your code that is going to catch it, so that's why it breaks and displays the exception. It doesn't know that the compiler-generated code will catch it. You can tell the IDE not to worry about uncaught exceptions.

VB.Net Task.WaitAll with Timeout Cancellation Request

I know there are a few threads and atricles about this arouynd but I just can't seem to get it to work.
Basically I have two (or more) tasks that I want to run but with a timeout and after this timeout I want all tasks to cancel and have a way to know that we've timed out so I can return this to my web page.
Here's what I have:
Dim cts As System.Threading.CancellationTokenSource = New System.Threading.CancellationTokenSource
Dim oTask1 As Task(Of SearchReturn) = Task(Of SearchReturn).Factory.StartNew(
Function()
While Not cts.IsCancellationRequested
cts.Token.ThrowIfCancellationRequested()
Return Test1(oTest)
End While
End Function, cts.Token)
Dim oTask2 As Task(Of SearchReturn) = Task(Of SearchReturn).Factory.StartNew(
Function()
While Not cts.IsCancellationRequested
cts.Token.ThrowIfCancellationRequested()
Return Test2(oTest)
End While
End Function, cts.Token)
If Not Task.WaitAll({oTask1, oTask2}, 4000, cts.Token) Then cts.Cancel()
cts.Cancel() does get called so it knows it's taken too long but it's not actually cancelling it, it just carries on until the tasks have finished.
I'm sure it's probably something simple I'm doing wrong but I jsut can't spot it.
Thanks in advance.
Andy

ContinueWith after PostAsJsonAsyc

I've got a vs2010, 4.0 vb.net, WinForms app calling AttemptLogin on a form load event.
I want to avoid blocking the form load if possible and I was hoping the tasks and continuation stuff in 4.0 would be the right way to go as I could run the main task on the default scheduler and the continuation on fromcurrentsynchronisationcontext but although I've got the OnAttemptLogin working I can't get my OnAttemptLoginCompleted function in the continuation to be called.
I think it's because the OnAttemptLogin returns a "RunToCompletion" task so the continuation never gets called. But I don't know how to deal with that, I've tried numerous things but I've now confused myself so much I'm pretty much mashing keys. Can anyone offer any advice? Am I simply doing it wrong or have I got the wrong idea all together?
Here's what I have so far, the OnAttemptLogin works as I would expect it to, but it then never calls the LongRunning tasks continuation.
Please note: I can't use await as I'm in vs2010 .net4.0 so I'm stuck with ContinueWith.
Public Sub AttemptLogin(OnAttemptLoginCompleted As Action(Of Task(Of HttpResponseMessage)))
Try
Dim LongRunningTask As Task(Of HttpResponseMessage) = Task.Factory.StartNew(Function()
Return OnAttemptLogin()
End Function, TaskScheduler.Default)
Dim UITask As Task(Of HttpResponseMessage) = LongRunningTask.ContinueWith(Sub(t)
OnAttemptLoginCompleted(t)
End Sub, TaskScheduler.FromCurrentSynchronizationContext)
LongRunningTask.Wait()
Catch ex As AggregateException
' nom nom nom
' do something useful
End Try
End Sub
Private Function OnAttemptLogin() As Task(Of HttpResponseMessage)
Dim aClient = New HttpClient()
Using (aClient)
' CREATE REQUEST
aClient.DefaultRequestHeaders.Accept.Add(New MediaTypeWithQualityHeaderValue("application/json"))
aClient.DefaultRequestHeaders.Add("Authorization", "Basic " + Convert.ToBase64String(System.Text.ASCIIEncoding.ASCII.GetBytes(String.Format("{0}:{1}", CallingDTO.Email, CallingDTO.Password))))
UserQueryDTO.UserName = UserDTO.Email
UserQueryDTO.Password = UserDTO.Password
Dim url As String = DnnRequest.GetUrl(Credentials.HttpAlias, cstModuleAssembly, "User", "CanLogin", False)
' POST REQUEST
Dim p As Task(Of HttpResponseMessage) = aClient.PostAsJsonAsync(url, UserQueryDTO).ContinueWith(Function(x)
' READ RESPONSE
Dim r = x.Result.Content.ReadAsAsync(Of HttpResponseMessage)()
r.Wait()
Return r.Result
End Function)
Try
p.Wait()
Catch ex As Exception
End Try
Return p
End Using
End Function
The problem here is ... convoluted. The main issue you have here, the reason why UITask won't run, is because LongRunningTask is not of type Task(Of HttpResponseMessage). It is actually a Task(Of Task(Of HttpResponseMessage)). OnAttempLogin() returns a Task(of H...), but the task that you start in form load is a Task that will return that Task, hence, Task(Of Task(Of ...)). So there's an exception in that line, hence the UITask line never runs. So the problem with the code is that there's too many Task things all over the place.
The other problem is that you aren't really doing anything asynchronously (except that part that never ran) since you are Wait()-ing for all the tasks. So you need to get rid of most of your waits to actually achieve that. Getting rid of the waits means you need to handle exceptions with a continuation.
Some minor points:
You don't really need the scheduler stuff, either.
UITask is simply a Task, not a Task(Of ...) since it doesn't return anything.
I'm continuing from UITask to handle exceptions so that it also catches UITask's exceptions. If I continued from LongRunningTask, I would miss those exceptions.
Below is an example of what I think the new code will look like. There may be a few syntax issues since I'm missing a few things to get this to compile:
Public Sub AttemptLogin(OnAttemptLoginCompleted As Action(Of Task(Of HttpResponseMessage)))
Dim LongRunningTask As Task(Of HttpResponseMessage) = OnAttemptLogin()
Dim UITask As Task = LongRunningTask.ContinueWith(AddressOf OnAttemptLoginCompleted)
uiTask.ContinueWith(Sub(t)
Dim ex As AggregateException = t.Exception
'nom nom nom
'all your exceptions will end up here.
End Sub, TaskContinuationOptions.OnlyOnFaulted)
End Sub
Private Function OnAttemptLogin() As Task(Of HttpResponseMessage)
Dim aClient = New HttpClient()
Using (aClient)
' CREATE REQUEST
aClient.DefaultRequestHeaders.Accept.Add(New MediaTypeWithQualityHeaderValue("application/json"))
aClient.DefaultRequestHeaders.Add("Authorization", "Basic " + Convert.ToBase64String(System.Text.ASCIIEncoding.ASCII.GetBytes(String.Format("{0}:{1}", CallingDTO.Email, CallingDTO.Password))))
UserQueryDTO.UserName = UserDTO.Email
UserQueryDTO.Password = UserDTO.Password
Dim url As String = DnnRequest.GetUrl(Credentials.HttpAlias, cstModuleAssembly, "User", "CanLogin", False)
' POST REQUEST
Dim p As Task(Of HttpResponseMessage) = aClient.PostAsJsonAsync(url, UserQueryDTO).ContinueWith(Function(x)
' READ RESPONSE
Dim r = x.Result.Content.ReadAsAsync(Of HttpResponseMessage)()
r.Wait()
Return r.Result
End Function)
Try
p.Wait()
Catch ex As Exception
End Try
Return p
End Using
End Function
my solution was to delete everything and give up, i will use something else, anything else, pff at this point ill lock the ui and not care, three days on this rubbish is crazy.
Marking jtseng's reply as correct even though it didnt work as hes the only reply and deserves something for taking the time to try and help.

Timing out a task using await in .NET 4.5

I'm trying out Async / Await in VB.NET 4.5 and would like my task
to timeout if it doesn't complete within a certain period. I have
Await Task.Run( Sub() PerformSomeAction() )
which seems neat. I also see there is a form of Task.Run that takes a cancellation token. How could I use this to cancel the task in case of some timeout?
EDIT
I have the following prototype solution
Dim cts = New CancellationTokenSource()
Dim ct As CancellationToken = cts.Token
Dim delay = Task.Delay(1000)
Dim completed = Await Task.WhenAny(Task.Run(Sub() PerfomSomeAction(ct), ct), delay)
If completed Is delay Then
cts.Cancel()
End If
This looks quite noisy code. Is this good? Another idea would be to handle
the timeout as an exception and use cts.CancelAfter. Something like this??
Dim cts = New CancellationTokenSource()
Dim ct As CancellationToken = cts.Token
try
cts.CancelAfter(1000) 'ms
Dim completed = Task.Run(Sub() PerformSomeAction(ct), ct)
catch SomeTimeoutException
end try
and withing PerformSomeAction I throw SomeTimeoutException if I get the
detect the cancelation token.
Tasks support cooperative cancellation, that means if you want to cancel a Task that's already running, the code inside the Task has to support cancellation and it decides when will it be canceled. The way to achieve this is to pass the cancellation token to the method and call ThrowIfCancellationRequested() at suitable places in the code of that method.
If the code in the Task doesn't support cancellation, there is no clean way to cancel or abort it.