0

We are developing a monolithic web application – very stateful. It handles both HTTP requests and long lived SignalR connections. (In ASP.NET Core 3.1 – we will upgrade to .NET 5 later.)

We do a redirect from a login page to our “main page”. The main page takes a while to load and initialize, after that it connects with SignalR. We also have a lot of work to do at the server side. Doing the server work in the login request (before redirecting to the main page) would slow down the login.

“Oh, let’s use a Task then!”, I thought. That is, put the server work in a Task, save that in the user state, and let it execute in parallel with the loading of the main page. Something like this (simplified):

public static async Task ServerSideInit()
{
    // do a lot of init work
}

// at the end of the controller handling the login page POST:
UserState.BackgroundTask = ServerSideInit();
Redirect(UrlToTheMainPage);

// when the main page connects via SignalR:
try {
    await UserState.BackgroundTask;
}
catch {
    // handle errors in the init work
}

This would really speed things up. It won’t matter if the page loading or the init work finishes first – we await the Task. And the work in ServerSideInit() isn’t critical. If something happens and the main page never connects, the UserState (and the Task) will be destroyed after a timeout – and that’s perfectly OK. (There are some caveats. We would e.g. have to use IServiceProvider to create/dispose a scope in ServerSideInit(), so we get a scoped DbContext outside of the controller. But that’s OK.)

But then I read that there is a risk the ASP.NET Core framework shuts down the Task when wrapping up the POST request! (Do you have to await async methods?) The simple HostingEnvironment.QueueBackgroundWorkItem isn’t available any longer. There is a new BackgroundService class, though. (https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-3.1&tabs=visual-studio) But registering a service and queueing jobs seems like a very cumbersome solution… We just want to fire a task that will take a couple of seconds to complete, and let that continue to run after ASP.NET Core has finished handling the POST request.

I’m not very experienced with ASP.NET Core… So I’d be very grateful for some input! Will my simple solution not work? Will the task be terminated by the framework? Is there some easier way to tell the framework “please don’t touch this Task”? Or is BackgroundService the way to go?

Liam
  • 27,717
  • 28
  • 128
  • 190
UglySwede
  • 429
  • 1
  • 7
  • 16
  • 1
    Is that task a static or dynamic? Means, will it be same for all users or different? – T.kowshik Yedida Feb 19 '21 at 10:47
  • Once you redirect you return HTTP response nd the server will stop processing. So what your doing doesn't make a lot of sense. If you want a "fire and forget" you should use `Task.Run()` to spawn a new thread, not `async` – Liam Feb 19 '21 at 10:49
  • Does this answer your question? [Simplest way to do a fire and forget method in c# 4.0](https://stackoverflow.com/questions/5613951/simplest-way-to-do-a-fire-and-forget-method-in-c-sharp-4-0) – Liam Feb 19 '21 at 10:49
  • *"execute in parallel"* async and parallel are different things – Liam Feb 19 '21 at 10:50
  • 1
    Here is a must-read article about this subject: [Fire and Forget on ASP.NET](https://blog.stephencleary.com/2014/06/fire-and-forget-on-asp-net.html) – Theodor Zoulias Feb 19 '21 at 11:14
  • @T.kowshikYedida: It will always be the same method, but with different parameters. Like the static method in my example, but I left out that the Task will have an object as parameter. (With both in parameters and properties to set as a result of the work.) Do you know an easy way to avoid that the ASP.NET Core framework will stop the Task when the framework has finished processing the HTTP request? – UglySwede Feb 19 '21 at 13:56
  • @Liam: Well, async is syntactic sugar (and not connected to await, really) – I could as well have used Task.Run(), but I let the complier handle the setup with async. With “parallel” I mean that I do server work while the HTTP redirect takes place, and the main page is loaded in the browser. So instead of the user having to wait, say, 3+4 seconds, he just have to wait 4 seconds. And it isn’t really “fire and forget” – I await the Task later on (which will return immediately if the Task is done), in the SignalR connection. After that I will be using the values calculated in the Task. This ... – UglySwede Feb 19 '21 at 13:59
  • @Liam: … should work nicely in C#, but documentation says the ASP.NET Core framework will detect there is a Task started and won’t allow it to continue to run! So I wonder if this is true and if there is a way around it. Before investing a lot of work in building it this way… :-) – UglySwede Feb 19 '21 at 14:00
  • @TheodorZoulias: Thanks for the tip! :-) But this is a different type of fire-and-forget – I use the Task later on. And the problem is not really the Task itself – the work isn’t important, so setting up some “distributed architecture” is overkill! The problem is that ASP.NET Core will stop the Task after finishing processing the HTTP request (according to the documentation). So I wonder if this is true and if there is an easy way around it. – UglySwede Feb 19 '21 at 14:01
  • @UglySwede may be you can use a push service and load the content needed when done? You can separate the container where this data resides and work on the remaining page? Is it on your cards? – T.kowshik Yedida Feb 20 '21 at 11:18

1 Answers1

3

Doing the server work in the login request (before redirecting to the main page) would slow down the login. “Oh, let’s use a Task then!”, I thought. That is, put the server work in a Task, save that in the user state, and let it execute in parallel with the loading of the main page.

So, you have a need for request-extrinsic work. I.e., work that your server does that is outside the scope of a request.

The first question you need to ask yourself is "does this work need to be done?" In other words, "am I OK with occasionally losing work?". If this work must be done for correctness reasons, then there is only one real solution: asynchronous messaging. If you're OK with occasionally losing work (e.g., if the main page will detect that the ServerSideInit is not done and will do it at that time), then what you're really talking about is a cache, and that's fine to have an in-memory solution for.

But then I read that there is a risk the ASP.NET Core framework shuts down the Task when wrapping up the POST request!

The first thing to recognize is that shutdowns are normal. Rolling updates during regular deployments, OS patches, etc... Your web server will voluntarily shut down sooner or later, and any code that assumes it will run forever is inherently buggy.

ASP.NET Core by default will consider itself "safe to shut down" when all requests have been responded to. This is the reasonable behavior for any HTTP service, and this logic extends to every HTTP framework, regardless of language or runtime. However, this is clearly a problem for request-extrinsic code.

So, if your code just starts a task by calling the method directly (or by Task.Run, another sadly popular option), then it is living dangerously: ASP.NET has no idea that request-extrinsic code even exists, and will happily exit when requested, abruptly terminating that code.

There are stopgap solutions like HostingEnvironment.QueueBackgroundWorkItem (pre-Core) and IHostedService / IHostApplicationLifetime (Core). These register the request-extrinsic code so that ASP.NET is aware of it, and will not shut down until that code completes. However, those solutions only go partway; since they are in-memory, they are also dangerous: ASP.NET is now aware of the request-extrinsic code, but HTTP proxies, load balancers, and deployment scripts are not.

Is there some easier way to tell the framework “please don’t touch this Task”?

Back to the question at the beginning of this answer: "does this work need to be done?"

If it's just an optimization and doesn't need to be done, then just firing off the work with a Task.Run (or IHostedService) should be sufficient. I wouldn't keep it in UserState, though, since Tasks aren't serializable.

If the work needs to be done, then build an asynchronous messaging solution.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Thank you for your kind and extensive answer! :-) Yes, I read up on IHostedService and such before posting my question. This is just an optimization. First, I have a login HTTP POST request, then our SPA connects via SignalR. The SPA page takes a while to load, and we also must do some other time consuming setup - by starting a Task in the POST request and await in in the SignalR OnConnectedAsync, these two things could be done in parallel. But the link I posted ... – UglySwede Feb 26 '21 at 16:48
  • ... made me uncertain - it said SynchronizationContext keeps track on the Tasks and throws an exception when the controller action finishes. But that doesn't seem to apply to Core - I've had time to test it now, and it works fine! :-) – UglySwede Feb 26 '21 at 16:49