简体   繁体   中英

Long running task in ApiController (using WebAPI, self-hosted OWIN)

I would like to run a long running task (say 4-5 minutes) in an ApiController within a self-hosted OWIN environment. However I would like to send a response back after starting that task (as soon as I started the long running task) without waiting it to finish. This long running task has nothing to do with the HTTP and sequentially runs some methods which may take very long.

I have a look at this blog post and decided to give a try to QueueBackgroundWorkItem . However, I am not sure if it is possible to use this method in a self-hosted (console application) owin environment or necessary to use it. In a self-hosted console application I think, the application itself manage the requests and all request run within the same AppDomain (applications default AppDomain, we do not create any new appDomain), so may be I can just run a long running task in a fire-and-forget fashion, without doing anything special?

Anyway, when I use QueueBackgroundWorkItem , I always get the error:

<Error>
<Message>An error has occurred.</Message>
<ExceptionMessage>
Operation is not valid due to the current state of the object.
</ExceptionMessage>
<ExceptionType>System.InvalidOperationException</ExceptionType>
<StackTrace>
at System.Web.Hosting.HostingEnvironment.QueueBackgroundWorkItem(Func`2 workItem) at BenchMarkService.SmokeTestController.IsItWorking() in C:\Devel\Code\Projects\BenchMarkService\BenchMarkService\SmokeTestController.cs:line 18 at lambda_method(Closure , Object , Object[] ) at System.Web.Http.Controllers.ReflectedHttpActionDescriptor.ActionExecutor.<>c__DisplayClass10.<GetExecutor>b__9(Object instance, Object[] methodParameters) at System.Web.Http.Controllers.ReflectedHttpActionDescriptor.ActionExecutor.Execute(Object instance, Object[] arguments) at System.Web.Http.Controllers.ReflectedHttpActionDescriptor.ExecuteAsync(HttpControllerContext controllerContext, IDictionary`2 arguments, CancellationToken cancellationToken) --- End of stack trace from previous location where exception was thrown --- at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at System.Web.Http.Controllers.ApiControllerActionInvoker.<InvokeActionAsyncCore>d__0.MoveNext() --- End of stack trace from previous location where exception was thrown --- at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at System.Web.Http.Controllers.ActionFilterResult.<ExecuteAsync>d__2.MoveNext() --- End of stack trace from previous location where exception was thrown --- at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at System.Web.Http.Dispatcher.HttpControllerDispatcher.<SendAsync>d__1.MoveNext()
</StackTrace>
</Error>

Also I found this question on SO and to be honest it seems a little bit confusing to me as the only way to achieve that is IRegisteredObject or not?

I am just trying to run a long running task in a self-hosted application and I appreciate any ideas about that. Almost all of the resources, questions I found are based on asp .net and I am not sure where to start.

This is pretty much what Hangfire was created to do ( https://www.hangfire.io/ ).

Sounds like a fire-and-forget job.

var jobId = BackgroundJob.Enqueue(
    () => Console.WriteLine("Fire-and-forget!"));

Self-hosted OWIN applications are typically regular console applications, therefore you can spin off some long running task in the same way you would do that in a console application, eg:

  • Task.Run
  • ThreadPool.QueueUserWorkItem
  • new Thread(...).Start()

In IIS hosted ASP.NET applications it is not advisable to use such methods because application pools are commonly recycled, hence your application gets shut down and restarted pretty often. In such a case your background task would be aborted. In order to prevent (or postpone) that, APIs like QueueBackgroundWorkItem have been introduced.

However because you are self-hosted and do not run in IIS, you can just use the APIs listed above.

You need to provide a mechanism for adding a task to something outside of the controller.

Typically in the apps I use owin for the actual host is IIS so I figured out I could use "Hosted Processes" but in a console app it's likely to be a lot simpler.

The most obvious approach that comes to mind is to pass in some sort of global object / singleton that your DI framework knows about can be sure is always the same object as a "container for the job"

public class FooController : ApiController
{
    ITaskRunner runner;

    public FooController(ITaskRunner runner) { this.runner = runner; }

   Public IActionResult DoStuff() {
       runner.AddTask(() => { Stuff(); });
       return Ok();
   }
}

The task runner could do the job by being little more than a simple wrapper around Task.Run() if you wanted, but having that hook there and passed in means that after the request is "handled" and the controller gets cleaned up, the task is still considered "in scope" so the application can continue to process it instead of attempting to clean it up.

I wanted to comment on the answer of War, but answering my own question seem better for a long explanation. This may not be a complete answer, sorry.

War's solution is a possible way to implement this feature and I did something similar to that. Basically, create a task store and whenever a new task is fired or completed add/remove this task to the task store. However, I would like to mention some issues, because I believe its not as simple as like that.

1) I agree with using a dependency injection framework in order to implement a singleton pattern. I used 'SingleInstance()' method of Autofac to do that. However, as you can see in many answers and resources on the Internet, singleton pattern is not popular pattern although sometimes seems to be indispensable.

2) Your repository should be thread-safe, so adding/removing tasks from that task store should be thread-safe. You can use a concurrent data structure like 'ConcurrentDictionary' instead of locking (which is a expensive operation in terms of cpu time).

3) You may consider using CancellationToken for your async. operations. It is highly possible that your application may be shutdown by user or an exception which can be handled may occur during the runtime. Long running tasks may be a bottleneck for graceful shutdown/restart operations especially if you have sensitive data to lose and use different resources like files which may be closed properly in that case. Although async. methods may not be cancelled immediately whenever you try to cancel the task by using the token, it is still worth giving a try to CancellationToken or similar mechanisms in order to stop/cancel your long running tasks whenever necessary.

4) Just adding a new task to your data store is not enough, also you have remove the task whwnever it completed whether successfully or not. I tried firing an event which signals the completion of task and then I removed this task from the task store. However, I am not happy with that because firing an event and then binding a method to this event to receive completion of a task is more or less like a continuation in TPL. It's like reinventing the wheel and also events may cause sneaky problems in a multithreaded environment.

Hope this helps. I prefer sharing some experiences instead of posting code, because I do not believe that I have an ultimate solution to this problem. War's response gives a rough idea, but you need to consider many issues before doing that in a production ready system.

Edit: For long running tasks you may have a look at this thread. It's a good practice to manage the thread pool.

PushStreamContent may be the answer you're looking for. PushStreamContent helps to stream the response. Looking at the following blog post to get some idea about the implementation. STREAMING DATA WITH ASP .NET WEB API AND PUSHCONTENTSTREAM

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM