2

I have the following problem: I need to execute a function that is delayed after processing the HTTP request. A user can assign for a certain task, after 45 minutes I have to check if the task is done. If not, I have to reopen the task for others.

I have tried the following code:

[HttpPost]
[ActionName("addJob")]
public string AddJob([FromBody] Task task)
{
   // Add task ...

   RemoveTaskAfterTime(task);

   return "Job has been added";
}

private async Task RemoveTaskAfterTime(Task task)
{
   System.Diagnostics.Debug.WriteLine("started to wait");
   await Task.Delay(5000);
   System.Diagnostics.Debug.WriteLine("remove task");
}

For some reason, "started to wait" gets called but "remove task" not. It works with Thread.sleep, but in that case also the response takes 45 minutes, so that´s no solution. Would be awesome if somebody could help me! Thank you in advance

Paul Groß
  • 207
  • 1
  • 8
  • 4
    Halting a thread for 45 minutes is not going to be a good idea. Find another way. – Jamiec Mar 30 '20 at 08:06
  • Make sure it's "async all the way down" if you're calling any async methods, make sure all methods in the call stack are async from the controller Action down. Weird things can happen if this is not the case. – phuzi Mar 30 '20 at 08:11
  • How about using a 45 minute timer that checks on the status of the task after it has elapsed? – Frauke Mar 30 '20 at 08:16
  • 1
    @Frauke I suppose that there will be not only one task. It sounds like one-minute timer that will check all `in-wait status` tasks and measure the interval how long they are waiting. – oleksa Mar 30 '20 at 08:37
  • So you want to [fire and forget on ASP.NET](https://blog.stephencleary.com/2014/06/fire-and-forget-on-asp-net.html). It is trickier than you think. – Theodor Zoulias Mar 30 '20 at 09:54

3 Answers3

2

Your problem is one of scope.

You probably haven't given any thought to this but AddJob is an instance method defined on a class. IIS handles the HTTP request by instantiating an object and calling the method. The child thread on which the Task runs is killed when the instance is disposed, because background threads are killed when all foreground threads of their owner are terminated. This is why your task starts but doesn't end.

If you want the Task to survive the object handling the request then you could make the task and its lifecycle management static. Of course that would not suit a server accepting any number of potentially concurrent requests, so the static Task would have to be a collection of Task into which you put the task object. We just introduced concurrency issues so you will need a thread-safe queue.

As soon as you start doing this sort of thing you take on responsibility for the object lifecycle, because it won't be garbage collected until you remove it from the collection.

You need a background process that periodically checks the time in queue for each of these objects and when they reach the required age the process should de-queue them and do whatever is supposed to happen when they reach the required age. This means you need to record the age of each task. You dequeue each task, check whether it's ripe and either process it or re-queue it.

Frankly I wouldn't use a Task object, I would create a class with properties for the housekeeping details and method implementing the behaviours. This is a combination of the Memento and Command design patterns.

As mentioned in another answer in a robust solution your tasks will survive server restarts. You can achieve this using Memento/Command and a persistent message queue in place of the memory queue. On Windows MSMQ is available for free. An advantage of this way is MSMQ takes over responsibility for thread safety in queue management.

To use an external message queue you will need to learn about (de)serialisation. Another answer uses a database server rather than a message queue to persist the serialised messages and this does work but it does not scale well. Purpose-built message queues rely a bunch of assumptions that can't be made in a general purpose database engine and this allows them to handle unplanned outages much more robustly and handle much higher levels of concurrency (or stress your server less for a given level of traffic).

Peter Wone
  • 17,965
  • 12
  • 82
  • 134
  • [Can .NET Task instances go out of scope during run?](https://stackoverflow.com/questions/2782802/can-net-task-instances-go-out-of-scope-during-run) No, because *the thread pool will still hold a reference to it, and prevent it from being garbage collected at least until completion.* – Theodor Zoulias Mar 30 '20 at 09:59
  • @TheodorZoulias From your own link: _After I answered this question (a long time ago!) I found out that it's not true that Tasks will always run to completion - there's a small, let's say "corner" case, where tasks may not finish. The reason for that is this: As I have answered previously, Tasks are essentially threads; but they are background threads. **Background threads are automatically aborted when all foreground threads finish.** So, if you don't do anything with the task and the program ends, there's a chance the task won't complete._ – Peter Wone Mar 30 '20 at 13:23
  • Indeed, an incomplete task will not keep a process alive. But what you have stated in your answer is that an incomplete task will be disposed when the method exits, which I don't think is correct. Also I don't hold as accurate that *"tasks are essentially threads"* or that *"tasks encapsulate threads"* from the accepted answer of the other question. A `Task` is a higher level abstraction than a `Thread`. You can't abort a `Task` for example. – Theodor Zoulias Mar 30 '20 at 13:37
  • @TheodorZoulias I have reworded to prevent others from misinterpreting me as you did. To be honest I have no idea what happens to the Task instance when its thread is killed like this, but it's totally irrelevant since my endgame doesn't use a Task anyhow. – Peter Wone Mar 30 '20 at 13:39
  • *The child thread is killed when the instance is disposed* <--- Even if this was true, which I don't think it is because AFAIK the ASP.NET engine aborts threads only when it recycles application domains, an aborted thread alone would not cause any incomplete `Task` to be disposed (unless the whole `AppDomain` was unloaded). – Theodor Zoulias Mar 30 '20 at 13:55
  • In the interest of accuracy I have removed the debatable irrelevant statement about dispose. – Peter Wone Mar 31 '20 at 01:20
2

I suppose that the problem is in the Task.Delay that was used.

Task.Delay should be used in async methods

45 minutes is too long to wait in the memory (however it is possible). What would you do with jobs are being waiting in memory if service (app pool, server whatever) is restarted ?

You can use the database to mark jobs as waiting using AddJob method. Job waiting start time should be set to check the job age later.

Then you can use the BackgroundService to check all waiting jobs age. You can do those checks each one minute (for example). Find jobs that are waiting more than 45 minutes and release them (set job status to available)

oleksa
  • 3,688
  • 1
  • 29
  • 54
1

Your controller action has to return Task<string> and be marked with async. Asynchronous methods used in the body of your action have to be awaited.

However, async/await is meant for shorter waits, usually network requests (eg. database or network service), not for 45 minute tasks. Client's browser connection will hit timeout in 1-2 minutes.

[HttpPost]
[ActionName("addJob")]
public async Task<string> AddJob([FromBody] Task task)
{
   // Add task ...

   await RemoveTaskAfterTime(task);

   return "Job has been added";
}

private async Task RemoveTaskAfterTime(Task task)
{
   System.Diagnostics.Debug.WriteLine("started to wait");
   await Task.Delay(5000);
   System.Diagnostics.Debug.WriteLine("remove task");
}
Nenad
  • 24,809
  • 11
  • 75
  • 93