Cancellable Async Web Request - c#

I am trying to make a generic method that will cancel an Async web request and any additional operations associated with it. I found another question regarding this such as this.
And I have written a helper class that will do just this. I present it below:
public static class Helpers
{
public static async Task<string> GetJson(string url,
CancellationTokenSource cancellationTokenSource,
bool useSynchronizationContext = true)
{
try
{
var request = (HttpWebRequest)WebRequest.Create(url);
string jsonStringResult;
using (WebResponse response = await request.GetResponseAsync()
.WithCancellation(cancellationTokenSource.Token,
request.Abort, useSynchronizationContext))
{
Stream dataStream = response.GetResponseStream();
StreamReader reader = new StreamReader(dataStream);
jsonStringResult = await reader.ReadToEndAsync();
reader.Close();
dataStream.Close();
}
cancellationTokenSource.Token.ThrowIfCancellationRequested();
return jsonStringResult;
}
catch (Exception ex) when (ex is OperationCanceledException
|| ex is TaskCanceledException)
{
}
catch (Exception ex) when (ex is WebException
&& ((WebException)ex).Status == WebExceptionStatus.RequestCanceled)
{
}
catch (Exception ex)
{
//Any other exception
}
finally
{
cancellationTokenSource.Dispose();
}
return default;
}
public static async Task<T> WithCancellation<T>(this Task<T> task,
CancellationToken cancellationToken, Action action,
bool useSynchronizationContext)
{
using (cancellationToken.Register(action, useSynchronizationContext))
{
return await task;
}
}
}
Notice the line
cancellationTokenSource.Token.ThrowIfCancellationRequested();
right before returning the JSON string.
When the operation is canceled and the flow of execution is on line
StreamReader reader = new StreamReader(dataStream);
for example, all lines below (reader.Close() etc.) will be executed and the exception will be thrown when ThrowIfCancelationRequested() is executed - is that correct? Am I missing something?
If so, is there a way to cancel everything at once?
Thanks everyone for their response,
After the answer provided and all really useful comments I updated the implementation.I used HttpClient and the extension method in the link for having a task that cannot actually being canceled to behave like one that can - readAsStringAsync is actually executed since it cannot accept a cancellation token.
public static class Helpers
{
public static async Task<string> GetJson(string url,CancellationToken cancellationToken)
{
try
{
string jsonStringResult;
using (var client = new HttpClient())
{
cancellationToken.ThrowIfCancellationRequested();
using (var response = await client.GetAsync(url, cancellationToken))
{
jsonStringResult = await response.Content.ReadAsStringAsync().WithCancellation(cancellationToken);
}
}
return jsonStringResult;
}
catch (Exception ex) when (ex is OperationCanceledException)
{
}
catch (Exception ex) when (ex is WebException exception && exception.Status == WebExceptionStatus.RequestCanceled)
{
}
catch (Exception ex)
{
//LogException(ex);
}
return default;
}
public static Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)
{
return task.IsCompleted
? task: task.ContinueWith(completedTask => completedTask.GetAwaiter().GetResult(),cancellationToken,TaskContinuationOptions.ExecuteSynchronously,TaskScheduler.Default);
}
}

all lines below (reader.Close() etc.) will be executed and the
exception will be thrown when ThrowIfCancelationRequested() is
executed - is that correct? Am I missing something?
If so, is there a way to cancel everything at once?
First of all, operation that you want to cancel has to explicitly support being canceled. So, you have to push your best to use a code for executing some operation in a way which somehow takes CancellationToken as an argument. If it is not possible, then you have no other chance.
That's why this concept is called Cooperative cancellation. Because almost always both sides should be aware of that cancellation happened. Client-side should be aware of that the code was actually canceled, it’s not enough for a client to know that cancellation was just requested. For the callee, it’s important to know about the fact that cancellation was requested in order to properly finish itself.
Regarding to checking whether operation is cancelled while executing Close method of stream and reader. You have to call cleanup methods always whether operation is cancelled or not if you want to avoid memory leaks. Of course I suggest you to use using statement which will automatically do that cleanup.
And by the way, for making some function cancelable you don't have to check whether cancellation is requested before executing each line. You just have to check whether cancellation is request before and after executing some long-running operation. And pass cancellation token if that long-running operations supports cancelling through cancellation token property.
Also, you have to take a look for side-effects. Don’t cancel if you’ve already incurred side-effects that your method isn’t prepared to revert on the way out that would leave you in an inconsistent state.
Some general code-block might be so:
if(ct.IsCancellationRequested)
{
break; // or throw
}
await DoSomething(ct);
if (ct.IsCancellationRequested)
{
// if there is no side-effect
return; // or throw
// or, we already did something in `DoSomething` method
// do some rollback
}
As a solution, you can use some different objects like HttpClient or WebRequest for executing async, awaitable and cancelable web request. You can take a look to that link for implementation details.

Related

Recursive Async/Await API call

I have an async method that makes an API call to a vendor. The vendor has requested that in the event of an error, we make an additional 2 calls with exactly the same information in the calls. My first thought for this scenario is to make my API method recursive. However, after looking at many questions on this site and other articles, I'm having a hard time grasping how async/await works with recursion.
My method (simplified for demonstration) follows. I'm not sure if I should be awaiting the recurisve call? Visual Studio throws the standard Because this call is not awaited, execution will continue...etc if I don't await the recursive call. It does appear to work when I do await the recursive call, I'm just afraid of what I'm doing to stack if I've done this incorrectly. Any help would be appreciated.
public async Task<string> GetData(int UserID, int Retry = 0)
{
try
{
var Request = new HttpRequestMessage(HttpMethod.Post, "myurlhere");
//Set other request info like headers and payload
var Response = await WebClient.SendAsync(Request);
return await Response.Content.ReadAsStringAsync();
}
catch (Exception Ex)
{
if (Retry <= 2)
{
Retry++;
return await GetData(UserID, Retry); //Should I await this?
}
else
{
return "";
}
}
}
It does appear to work when I do await the recursive call,
Grand, so.
I'm just afraid of what I'm doing to stack if I've done this incorrectly.
Because awaiting Tasks that don't return synchronously is handled by a state machine, it is in fact less burden on the stack in such a case than if it was "normal" non-async code. The recursive call becomes another state machine and a reference to it is a field in the first state machine.
It's possible to take an iterative rather than recursive approach to this generally (have a loop and exit on first success) but really since even the non-async equivalent wouldn't have significant stack pressure there's no need to do things any differently than you have done.
This is not a situation where recursion is needed. As others have suggested I would use something like this:
public async Task<string> GetDataWithRetry(int UserID, int Tries = 1)
{
Exception lastexception = null;
for (int trycount=0; trycount < tries; trycount++)
try
{
return await GetData(UserID);
}
catch (Exception Ex)
{
lastexception = Ex;
}
throw lastexception;
}
public async Task<string> GetData(int UserID)
{
var Request = new HttpRequestMessage(HttpMethod.Post, "myurlhere");
//Set other request info like headers and payload
var Response = await WebClient.SendAsync(Request);
return await Response.Content.ReadAsStringAsync();
}

Any way to differentiate Cancel and Timeout

I have some code that is validating some data by making calls to a number of other services. I start all of the calls in parallel and then wait until at least one of them finishes. If any of the requests fail, I don't care about the result of the other calls.
I make the calls with HttpClient and I have passed an HttpMessageHandler in that does a bunch of logging. Essentially:
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
HttpResponseMessage response = null;
try
{
response = await base.SendAsync(request, cancellationToken);
}
catch (OperationCanceledException ex)
{
LogTimeout(...);
throw;
}
catch (Exception ex)
{
LogFailure(...);
throw;
}
finally
{
LogComplete(...);
}
return response;
}
No the part that I'm having trouble with is when I cancel the requests. When I cancel a request, I'm doing it on purpose, so I don't want it to get logged as a timeout, but there doesn't appear to be any difference between a cancellation and a real timeout.
Is there anyway to accomplish this?
Edit: I need to clarify this, a little bit. The service making the calls in parallel is passing in CancellationTokens with a timeout:
var ct = new CancellationTokenSource(TimeSpan.FromSeconds(2));
So when the server takes more than two seconds to respond, I get an OperationCanceledException, and if I manually cancel the token source (say because another server returned an error after 1 second), then I still get an OperationCanceledException. Ideally, I would be able to look at CancellationToken.IsCancellationRequested to determine if it was cancelled due to a timeout, as opposed to explicitly requested to be cancelled, but it appears that you get the same value regardless of how it was canceled.
If you want to distinguish the two cancellation types, then you need to use two different cancellation tokens. There's no other way. This is not too hard since they can be linked - just a bit awkward.
The cleanest way to write this IMO is to move the timeout code into the SendAsync method instead of the calling method:
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
{
cts.CancelAfter(TimeSpan.FromSeconds(2));
try
{
return await base.SendAsync(request, cts.Token);
}
catch (OperationCanceledException ex)
{
if (cancellationToken.IsCancellationRequested)
return null;
LogTimeout(...);
throw;
}
catch (Exception ex)
{
LogFailure(...);
throw;
}
finally
{
LogComplete(...);
}
}
}
If you don't want to move the timeout code into SendAsync, then you'll need to do the logging outside of that method, too.
If the exceptions aren't telling you the difference between the two cases then you will need to check with either the Task or the CancellationToken to see if there was actually a cancellation.
I would lean toward asking the Task which will have its IsCanceled property return true if an unhandled OperationCanceledException was thrown (using CancellationToken.ThrowIfCancellationRequested inside base.SendAsync most likely). Something like this...
HttpResponseMessage response = null;
Task sendTask = null;
try
{
sendTask = base.SendAsync(request, cancellationToken);
await sendTask;
}
catch (OperationCanceledException ex)
{
if (!sendTask.IsCancelled)
{
LogTimeout(...);
throw;
}
}
EDIT
In response to the update to the question, I wanted to update my answer. You are right cancellation whether it is specifically requested on the CancellationTokenSource or if it is caused by a timeout will lead to exactly the same outcome. If you decompile CancellationTokenSource you will see that for the timeout it just sets a Timer callback that will explicitly call CancellationTokenSource.Cancel when the timeout is reached, so both ways will end up calling the same Cancel method.
I think if you want to tell the difference you will need to derive from CancellationTokenSource (it isn't a sealed class) and then add your own custom cancel method that will set a flag to let you know that you explicitly cancelled the operation rather than letting it time out.
This is unfortunate since you will have both your custom cancel method and the original Cancel method available and will have to be sure to use the custom one. You may be able to get away with your custom logic just hiding the existing Cancel operation with something like this:
class CustomCancellationTokenSource : CancellationTokenSource
{
public bool WasManuallyCancelled {get; private set;}
public new void Cancel()
{
WasManuallyCancelled = true;
base.Cancel();
}
}
I would think that hiding the base method will work, you can give it a shot and find out.

Can I cancel StreamReader.ReadLineAsync with a CancellationToken?

When I cancel my async method with the following content by calling the Cancel() method of my CancellationTokenSource, it will stop eventually. However since the line Console.WriteLine(await reader.ReadLineAsync()); takes quite a bit to complete, I tried to pass my CancellationToken to ReadLineAsync() as well (expecting it to return an empty string) in order to make the method more responsive to my Cancel() call. However I could not pass a CancellationToken to ReadLineAsync().
Can I cancel a call to Console.WriteLine() or Streamreader.ReadLineAsync() and if so, how do I do it?
Why is ReadLineAsync() not accepting a CancellationToken? I thought it was good practice to give async methods an optional CancellationToken parameter even if the method still completes after being canceled.
StreamReader reader = new StreamReader(dataStream);
while (!reader.EndOfStream)
{
if (ct.IsCancellationRequested){
ct.ThrowIfCancellationRequested();
break;
}
else
{
Console.WriteLine(await reader.ReadLineAsync());
}
}
Update:
Like stated in the comments below, the Console.WriteLine() call alone was already taking up several seconds due to a poorly formatted input string of 40.000 characters per line. Breaking this down solves my response-time issues, but I am still interested in any suggestions or workarounds on how to cancel this long-running statement if for some reason writing 40.000 characters into one line was intended (for example when dumping the whole string into a file).
.NET 6 brings Task.WaitAsync(CancellationToken). So one can write:
using StreamReader reader = new StreamReader(dataStream);
while (!reader.EndOfStream)
{
Console.WriteLine(await reader.ReadLineAsync().WaitAsync(cancellationToken).ConfigureAwait(false));
}
In .NET 7 (not yet released), it should be possible to write simply:
using StreamReader reader = new StreamReader(dataStream);
while (!reader.EndOfStream)
{
Console.WriteLine(await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false);
}
based on https://github.com/dotnet/runtime/issues/20824 and https://github.com/dotnet/runtime/pull/61898.
You can't cancel the operation unless it's cancellable. You can use the WithCancellation extension method to have your code flow behave as if it was cancelled, but the underlying would still run:
public static Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)
{
return task.IsCompleted // fast-path optimization
? task
: task.ContinueWith(
completedTask => completedTask.GetAwaiter().GetResult(),
cancellationToken,
TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default);
}
Usage:
await task.WithCancellation(cancellationToken);
You can't cancel Console.WriteLine and you don't need to. It's instantaneous if you have a reasonable sized string.
About the guideline: If your implementation doesn't actually support cancellation you shouldn't be accepting a token since it sends a mixed message.
If you do have a huge string to write to the console you shouldn't use Console.WriteLine. You can write the string in a character at a time and have that method be cancellable:
public void DumpHugeString(string line, CancellationToken token)
{
foreach (var character in line)
{
token.ThrowIfCancellationRequested();
Console.Write(character);
}
Console.WriteLine();
}
An even better solution would be to write in batches instead of single characters. Here's an implementation using MoreLinq's Batch:
public void DumpHugeString(string line, CancellationToken token)
{
foreach (var characterBatch in line.Batch(100))
{
token.ThrowIfCancellationRequested();
Console.Write(characterBatch.ToArray());
}
Console.WriteLine();
}
So, in conclusion:
var reader = new StreamReader(dataStream);
while (!reader.EndOfStream)
{
DumpHugeString(await reader.ReadLineAsync().WithCancellation(token), token);
}
I generalized this answer to this:
public static async Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken, Action action, bool useSynchronizationContext = true)
{
using (cancellationToken.Register(action, useSynchronizationContext))
{
try
{
return await task;
}
catch (Exception ex)
{
if (cancellationToken.IsCancellationRequested)
{
// the Exception will be available as Exception.InnerException
throw new OperationCanceledException(ex.Message, ex, cancellationToken);
}
// cancellation hasn't been requested, rethrow the original Exception
throw;
}
}
}
Now you can use your cancellation token on any cancelable async method. For example WebRequest.GetResponseAsync:
var request = (HttpWebRequest)WebRequest.Create(url);
using (var response = await request.GetResponseAsync())
{
. . .
}
will become:
var request = (HttpWebRequest)WebRequest.Create(url);
using (WebResponse response = await request.GetResponseAsync().WithCancellation(CancellationToken.None, request.Abort, true))
{
. . .
}
See example http://pastebin.com/KauKE0rW
I like to use an infinite delay, the code is quite clean.
If waiting is complete WhenAny returns and the cancellationToken will throw. Else, the result of task will be returned.
public static async Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)
{
using (var delayCTS = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
{
var waiting = Task.Delay(-1, delayCTS.Token);
var doing = task;
await Task.WhenAny(waiting, doing);
delayCTS.Cancel();
cancellationToken.ThrowIfCancellationRequested();
return await doing;
}
}
You can't cancel Streamreader.ReadLineAsync(). IMHO this is because reading a single line should be very quick. But you can easily prevent the Console.WriteLine() from happening by using a separate task variable.
The check for ct.IsCancellationRequested is also redundand as ct.ThrowIfCancellationRequested() will only throw if cancellation is requested.
StreamReader reader = new StreamReader(dataStream);
while (!reader.EndOfStream)
{
ct.ThrowIfCancellationRequested();
string line = await reader.ReadLineAsync());
ct.ThrowIfCancellationRequested();
Console.WriteLine(line);
}

Async network operations never finish

I have several asynchronous network operations that return a task that may never finish:
UdpClient.ReceiveAsync doesn't accept a CancellationToken
TcpClient.GetStream returns a NetworkStream that doesn't respect the CancellationToken on Stream.ReadAsync (checking for cancellation only at the start of the operation)
Both wait for a message that may never come (because of packet loss or no response for example). That means I have phantom tasks that never complete, continuations that will never run and used sockets on hold. I know i can use TimeoutAfter, but that will only fix the continuation problem.
So what am I supposed to do?
So i've made an extension method on IDisposable that creates a CancellationToken that disposes the connection on timeout, so the task finishes and everything carries on:
public static IDisposable CreateTimeoutScope(this IDisposable disposable, TimeSpan timeSpan)
{
var cancellationTokenSource = new CancellationTokenSource(timeSpan);
var cancellationTokenRegistration = cancellationTokenSource.Token.Register(disposable.Dispose);
return new DisposableScope(
() =>
{
cancellationTokenRegistration.Dispose();
cancellationTokenSource.Dispose();
disposable.Dispose();
});
}
And the usage is extremely simple:
try
{
var client = new UdpClient();
using (client.CreateTimeoutScope(TimeSpan.FromSeconds(2)))
{
var result = await client.ReceiveAsync();
// Handle result
}
}
catch (ObjectDisposedException)
{
return null;
}
Extra Info:
public sealed class DisposableScope : IDisposable
{
private readonly Action _closeScopeAction;
public DisposableScope(Action closeScopeAction)
{
_closeScopeAction = closeScopeAction;
}
public void Dispose()
{
_closeScopeAction();
}
}
So what am I supposed to do?
In this particular case, I would rather use UdpClient.Client.ReceiveTimeout and TcpClient.ReceiveTimeout to time out a UDP or TCP receive operation gracefully. I'd like to have the time-out error coming from the socket, rather than from any external source.
If in addition to that I need to observe some other cancellation event, like a UI button click, I would just use WithCancellation from Stephen Toub's "How do I cancel non-cancelable async operations?", like this:
using (var client = new UdpClient())
{
UdpClient.Client.ReceiveTimeout = 2000;
var result = await client.ReceiveAsync().WithCancellation(userToken);
// ...
}
To address the comment, in case ReceiveTimeout has no effect on ReceiveAsync, I'd still use WithCancellation:
using (var client = new UdpClient())
using (var cts = CancellationTokenSource.CreateLinkedTokenSource(userToken))
{
UdpClient.Client.ReceiveTimeout = 2000;
cts.CancelAfter(2000);
var result = await client.ReceiveAsync().WithCancellation(cts.Token);
// ...
}
IMO, this more clearly shows my intentions as a developer and is more readable to a 3rd party. Also, I don't need to catch ObjectDisposedException exeception. I still need to observe OperationCanceledException somewhere in my client code which calls this, but I'd be doing that anyway. OperationCanceledException usually stands out from other exceptions, and I have an option to check OperationCanceledException.CancellationToken to observe the reason for cancellation.
Other than that, there's not much difference from #I3arnon's answer. I just don't feel like I need another pattern for this, as I already have WithCancellation at my disposal.
To further address the comments:
I'd only be catching OperationCanceledException in the client code, i.e.:
async void Button_Click(sender o, EventArgs args)
{
try
{
await DoSocketStuffAsync(_userCancellationToken.Token);
}
catch (Exception ex)
{
while (ex is AggregateException)
ex = ex.InnerException;
if (ex is OperationCanceledException)
return; // ignore if cancelled
// report otherwise
MessageBox.Show(ex.Message);
}
}
Yes, I'll be using WithCancellation with each ReadAsync call and I like that fact, for the following reasons. Firstly, I can create an extension ReceiveAsyncWithToken:
public static class UdpClientExt
{
public static Task<UdpReceiveResult> ReceiveAsyncWithToken(
this UdpClient client, CancellationToken token)
{
return client.ReceiveAsync().WithCancellation(token);
}
}
Secondly, in 3yrs from now I may be reviewing this code for .NET 6.0. By then, Microsoft may have a new API, UdpClient.ReceiveAsyncWithTimeout. In my case, I'll simply replace ReceiveAsyncWithToken(token) or ReceiveAsync().WithCancellation(token) with ReceiveAsyncWithTimeout(timeout, userToken). It would not be so obvious to deal with CreateTimeoutScope.

Distinguish timeout from user cancellation

HttpClient has a builtin timeout feature (despite being all asynchronous, i.e. timeouts could be considered orthogonal to the http request functionality and thus be handled by generic asynchronous utilities, but that aside) and when the timeout kicks in, it'll throw a TaskCanceledException (wrapped in an AggregateException).
The TCE contains a CancellationToken that equals CancellationToken.None.
Now if I provide HttpClient with a CancellationToken of my own and use that to cancel the operation before it finishes (or times out), I get the exact same TaskCanceledException, again with a CancellationToken.None.
Is there still a way, by looking only at the exception thrown, to figure out whether a timeout canceled the request, without having to make my own CancellationToken accessible to the code that checks the exception?
P.S. Could this be a bug and CancellationToken got somehow wrongly fixed to CancellationToken.None? In the cancelled using custom CancellationToken case, I'd expect TaskCanceledException.CancellationToken to equal that custom token.
Edit
To make the problem a bit more clear, with access to the original CancellationTokenSource, it is easy to distinguish timeout and user cancellation:
origCancellationTokenSource.IsCancellationRequested == true
Getting the CancellationToken from the exception though gives the wrong answer:
((TaskCanceledException) e.InnerException).CancellationToken.IsCancellationRequested == false
Here a minimal example, due to popular demand:
public void foo()
{
makeRequest().ContinueWith(task =>
{
try
{
var result = task.Result;
// do something with the result;
}
catch (Exception e)
{
TaskCanceledException innerException = e.InnerException as TaskCanceledException;
bool timedOut = innerException != null && innerException.CancellationToken.IsCancellationRequested == false;
// Unfortunately, the above .IsCancellationRequested
// is always false, no matter if the request was
// cancelled using CancellationTaskSource.Cancel()
// or if it timed out
}
});
}
public Task<HttpResponseMessage> makeRequest()
{
var cts = new CancellationTokenSource();
HttpClient client = new HttpClient() { Timeout = TimeSpan.FromSeconds(10) };
HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "url");
passCancellationTokenToOtherPartOfTheCode(cts);
return client.SendAsync(httpRequestMessage, cts.Token);
}
The accepted answer is certainly how this should work in theory, but unfortunately in practice IsCancellationRequested does not (reliably) get set on the token that is attached to the exception:
Cancelling an HttpClient Request - Why is TaskCanceledException.CancellationToken.IsCancellationRequested false?
Yes, they both return the same exception (possibly because of timeout internally using a token too) but it can be easily figured out by doing this:
catch (OperationCanceledException ex)
{
if (token.IsCancellationRequested)
{
return -1;
}
return -2;
}
so basically if you hit the exception but your token is not cancelled, well it was a regular http timeout

Categories

Resources