ApiController 中长时间运行的任务(使用 WebAPI,自托管 OWIN)
Posted
技术标签:
【中文标题】ApiController 中长时间运行的任务(使用 WebAPI,自托管 OWIN)【英文标题】:Long running task in ApiController (using WebAPI, self-hosted OWIN) 【发布时间】:2016-12-19 09:38:42 【问题描述】:我想在自托管 OWIN 环境中的 ApiController 中运行一个长时间运行的任务(比如 4-5 分钟)。但是,我想在启动该任务后(一旦我开始长时间运行的任务)就发回响应而不等待它完成。这个长时间运行的任务与 HTTP 无关,顺序运行一些可能需要很长时间的方法。
我看了this 的博文,决定试试QueueBackgroundWorkItem
。但是,我不确定是否可以在自托管(控制台应用程序)owin 环境中使用此方法或是否有必要使用它。我认为在自托管控制台应用程序中,应用程序本身管理请求并且所有请求都在同一个 AppDomain 中运行(应用程序默认 AppDomain,我们不创建任何新的 appDomain),所以我可以只运行一个长时间运行的任务无需做任何特别的事情,即刻即忘的时尚?
无论如何,当我使用QueueBackgroundWorkItem
时,我总是得到错误:
<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>
我还发现了关于 SO 的 this 问题,老实说,这对我来说似乎有点令人困惑,因为实现这一目标的唯一方法是 IRegisteredObject
?
我只是想在自托管应用程序中运行一个长时间运行的任务,我很欣赏任何关于此的想法。我发现的几乎所有资源和问题都是基于 asp .net 的,我不知道从哪里开始。
【问题讨论】:
将任务(在数据库中)排队并让一些服务处理它不是一个选项吗? Hangfire (hangfire.io) 据我所知做了类似的事情。但是,我认为这与问题有点不同。 【参考方案1】:自托管的 OWIN 应用程序通常是常规的控制台应用程序,因此您可以像在控制台应用程序中那样分拆一些长时间运行的任务,例如:
Task.Run
ThreadPool.QueueUserWorkItem
new Thread(...).Start()
在 IIS 托管的 ASP.NET 应用程序中,不建议使用此类方法,因为应用程序池通常会被回收,因此您的应用程序会经常关闭并重新启动。在这种情况下,您的后台任务将被中止。为了防止(或推迟)这种情况,我们引入了 QueueBackgroundWorkItem
等 API。
但是,由于您是自托管的并且不在 IIS 中运行,您可以只使用上面列出的 API。
【讨论】:
【参考方案2】:这几乎就是 Hangfire 创建的目的 (https://www.hangfire.io/)。
听起来像是一劳永逸的工作。
var jobId = BackgroundJob.Enqueue(
() => Console.WriteLine("Fire-and-forget!"));
【讨论】:
如果是请求-响应怎么办?我可以考虑将操作结果写入数据库,然后从客户端轮询它。但除此之外,我们还能做什么?【参考方案3】:您需要提供一种将任务添加到控制器外部的机制。
通常在我使用 owin 作为实际主机的应用程序中是 IIS,所以我想我可以使用“托管进程”,但在控制台应用程序中它可能要简单得多。
想到的最明显的方法是传入某种全局对象/单例,你的 DI 框架知道可以肯定总是与“作业容器”相同的对象
public class FooController : ApiController
ITaskRunner runner;
public FooController(ITaskRunner runner) this.runner = runner;
Public IActionResult DoStuff()
runner.AddTask(() => Stuff(); );
return Ok();
如果你愿意的话,任务运行器可以通过简单地封装 Task.Run() 来完成这项工作,但是在那儿有那个钩子并传入意味着在请求被“处理”并且控制器被清理之后起,该任务仍被视为“在范围内”,因此应用程序可以继续处理它,而不是尝试清理它。
【讨论】:
【参考方案4】:我想评论战争的答案,但回答我自己的问题似乎更适合长篇大论。这可能不是一个完整的答案,抱歉。
War 的解决方案是实现此功能的一种可能方式,我做了类似的事情。基本上,创建一个任务存储,每当新任务被触发或完成时,将此任务添加/删除到任务存储。但是,我想提一些问题,因为我相信它没有那么简单。
1) 我同意使用依赖注入框架来实现单例模式。我使用 Autofac 的“SingleInstance()”方法来做到这一点。但是,正如您在互联网上的许多答案和资源中看到的那样,单例模式并不是流行的模式,尽管有时似乎是必不可少的。
2) 您的存储库应该是线程安全的,因此从该任务存储中添加/删除任务应该是线程安全的。您可以使用像“ConcurrentDictionary”这样的并发数据结构来代替锁定(这在 cpu 时间方面是一项昂贵的操作)。
3) 您可以考虑将 CancellationToken 用于异步。操作。您的应用程序很有可能被用户关闭,或者在运行时可能发生可以处理的异常。长时间运行的任务可能是正常关闭/重启操作的瓶颈,特别是如果您有敏感数据要丢失并使用不同的资源(如在这种情况下可能会正确关闭的文件)。虽然异步。每当您尝试使用令牌取消任务时,方法可能不会立即取消,仍然值得尝试使用 CancellationToken 或类似机制,以便在必要时停止/取消长时间运行的任务。
4) 仅仅向您的数据存储添加一个新任务是不够的,无论它是否成功完成,您都必须删除该任务。我尝试触发一个表明任务完成的事件,然后我从任务存储中删除了这个任务。但是,我对此并不满意,因为触发事件然后将方法绑定到该事件以接收任务的完成或多或少类似于 TPL 中的延续。这就像重新发明***一样,事件也可能在多线程环境中引起偷偷摸摸的问题。
希望这会有所帮助。我更喜欢分享一些经验而不是发布代码,因为我不相信我对这个问题有最终的解决方案。 War 的回答给出了一个粗略的想法,但在生产就绪系统中这样做之前,您需要考虑许多问题。
编辑:对于长时间运行的任务,您可以查看this 线程。管理线程池是一种很好的做法。
【讨论】:
是的,我的想法完全正确......我试图不让我的答案陷入太多背景信息,这些背景信息可能会或可能不会对您的实施做出假设。对于一个完整的解决方案,您希望避免做一些非常简单的事情来满足您的代码库中的模式/编码标准。至于线程问题,你的数据层/业务逻辑应该处理它,所以在这个级别我不会担心启动线程/任务。我的 2p ......很高兴你解决了它:) 只有一条小评论。 ConcurrentDictionary 使用锁定,因此它不会消除对性能的影响,它只是将其从您的应用程序中抽象出来。不错的文章,否则。【参考方案5】:PushStreamContent 可能是您正在寻找的答案。 PushStreamContent 有助于流式传输响应。查看以下博客文章以了解有关实施的一些想法。 STREAMING DATA WITH ASP .NET WEB API AND PUSHCONTENTSTREAM
【讨论】:
以上是关于ApiController 中长时间运行的任务(使用 WebAPI,自托管 OWIN)的主要内容,如果未能解决你的问题,请参考以下文章