Web API - Fire and Forget

C # HttpWebRequest , Fire and forget an API call

First, make fully async version of your code

using System.Threading;

public async Task<System.Net.WebResponse> SendRequestAsync(
string inputXML, string url, CancellationToken cancellationToken)
{
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
request.Method = "POST";
request.ContentType = "application/xml";

byte[] requestBytes = Encoding.UTF8.GetBytes(inputXML);

// GetRequestStreamAsync is a lightweight operation I assume
// so we don't really need any cancellation
using (Stream requestStream = await request.GetRequestStreamAsync())
{
// Idk if we really need cancellation here, but just in case of big request we might need to
await requestStream.WriteAsync(
requestBytes, 0, requestBytes.Length, cancellationToken);
}

// Close any long-running request
using (cancellationToken.Register(() => request.Abort(), useSynchronizationContext: false))
{
var response = await request.GetResponseAsync();
cancellationToken.ThrowIfCancellationRequested();
return response;
}
}

Let's create an async void method, but make it safe. It will basically execute in "fire-and-forget" manner.

public async void DoWork(string inputXML, string url, CancellationToken ct)
{
try
{
using(var response = await SendRequestAsync(inputXML, url, ct))
{
var httpResponse = (HttpWebResponse) response;
// Use 201 Created or whatever you need
if (httpResponse.StatusCode != HttpStatusCode.Created)
{
// TODO: handle wrong status code
}
}
}
catch (Exception e)
{
if (ct.IsCancellationRequested)
{
Console.WriteLine("Cancelled");
}
else
{
// TODO: handle exception
}
}
}

private static CancellationTokenSource _cts = new CancellationTokenSource();

public static void Main (string[] args)
{
DoWork("xml string", "example.com", cts.Token);
Console.WriteLine("Boom!");
if (Console.ReadLine() == "exit")
{
// Cancel the damn job
cts.Cancel();
}
}

It's important to handle all errors from inside a DoWork, because following will not work

// Warning, will NOT catch an exception
public static void Main (string[] args)
{
try
{
DoWork("xml string", "example.com");
}
catch (Exception e)
{
}
}

EDIT: OP requested cancellation so I added cancellation

Fire-and-forget in .Net Core Web API - approaches and observations

As you mentioned in comments, the services referenced MyService are not disposable.

That is, when the DI scope is closed, nothing happens with those instances, and the code which has references to those services can access them.

However, you’re worrying right, that there might be issues with the services when they are transient and they are used like in your fire-and-forget example. You solution is right - the Submit method lifetime doesn’t depend on the Request lifetime, and vice versa, therefore yes, you create a separate scope for this.

I understand that both solutions are technically acceptable, correct

I see here only one solution - the one with scope. If you mean the original code, then it is a bad idea, even if it works now. It will break once you add any dependency to your service, which can be disposed(or its dependency, or … and so on).

So, answering the questions:

  1. See this answer
  2. Yes
  3. Yes
  4. I don’t see improvements here apart from syntax sugar, which everyone puts depending on the taste.

P.S. you can see this answer it may help to understand di lifecycle

Safe way to implement a Fire and Forget method on ASP.NET Core

Re : Unawaited call to an async method vs Task.Run()

Since there's only a small amount of CPU bound work in Post (i.e. creating json payload), there's no benefit of another Task.Run - the overhead of scheduling a new Task on the Threadpool will outweigh any benefit IMO. i.e.

Post(action, message, LogLevel.Info);*/ // Or should I just use it like this?

is the better of the two approaches. You'll likely want to suppress the compiler warning associated within unawaited Tasks and leave a comment for the next dev to come across the code.

But as per Stephen Cleary's definitive answer, fire and forget in ASP.Net is almost never a good idea. Preferable would be to offload work, e.g. via a queue, to a Windows Service, Azure Web Job et al.

There are additional dangers - if the unawaited Task throws, you'll want to observe the exception.

Also, note that any work done after the Post (e.g. if you work with response) that this is still a continuation Task which needs to be scheduled on the Threadpool - if you fire off high volumes of your Post method, you'll wind up with a lot of thread contention when they complete.

Re : Also, if I don't use await with Task.Run(), will I block thread?

await doesn't require a thread. await is syntactic sugar to ask the compiler to rewrite your code asynchronously.
Task.Run() will schedule a second task on the ThreadPool, which will only do a tiny amount of work before it hits the PostAsync method, which is why the recommendation is not to use it.

The amount of caller thread usage/block on the unawaited call from Info to Post depends on what kind of work is done before the Task is returned.
In your case the Json serialization work would be done on the caller's thread (I've labelled #1), however the execution time should be negligible in comparison to the HTTP call duration. So although not awaited by method Info, any code after the HTTP call will still need to be scheduled when the Http call completes, and will be scheduled on any available thread (#2).

public void Info(string action, string message)
{
#pragma warning disable 4014 // Deliberate fire and forget
Post(action, message, LogLevel.Info); // Unawaited Task, thread #1
#pragma warning restore 4014
}

private async Task Post(string action, string message, LogLevel logLevel)
{
var jsonData = JsonConvert.SerializeObject(log); // #1
var content = new StringContent(jsonData, Encoding.UTF8, "application/json"); // #1

var response = await httpClient.PostAsync(...), content);

// Work here will be scheduled on any available Thread, after PostAsync completes #2
}

Re: Exception Handling

try..catch blocks work with async code - await will check for a faulted Task and raise an exception:

 public async Task Post()
{
try
{
// ... other serialization code here ...
await HttpPostAsync();
}
catch (Exception ex)
{
// Do you have a logger of last resort?
Trace.WriteLine(ex.Message);
}
}

Although the above will meet the criteria for observing the exception, it is still a good idea to register an UnobservedTaskException handler at the global level.

This will help you detect and identify where you've failed to observe an exception:

TaskScheduler.UnobservedTaskException += (sender, eventArgs) =>
{
eventArgs.SetObserved();
((AggregateException)eventArgs.Exception).Handle(ex =>
{
// Arriving here is BAD - means we've forgotten an exception handler around await
// Or haven't checked for `.IsFaulted` on `.ContinueWith`
Trace.WriteLine($"Unobserved Exception {ex.Message}");
return true;
});
};

Note that the above handler is only triggered when the Task is collected by the GC, which can be some time after the occurrence of the exception.



Related Topics



Leave a reply



Submit