如何从 WebAPI 后台服务干净退出

How to exit clean from WebAPI background service

下面的代码是代表 SPA 打印的 Web API。为简洁起见,我省略了 using 语句和实际的打印逻辑。那东西一切正常。兴趣点是将打印逻辑重构到后台线程,使用 web api 方法将作业排入队列。我这样做是因为快速连续发送的打印作业相互干扰,只有最后一个作业打印。

它解决了序列化打印作业的问题,但提出了如何检测关机并发出循环终止信号的问题。

namespace WebPrint.Controllers
{
    public class LabelController : ApiController
    {
        static readonly ConcurrentQueue<PrintJob> queue = new ConcurrentQueue<PrintJob>();
        static bool running = true;
        static LabelController()
        {
            ThreadPool.QueueUserWorkItem((state) => {
                while (running)
                {
                    Thread.Sleep(30);
                    if (queue.TryDequeue(out PrintJob job))
                    {
                        this.Print(job);
                    }
                }
            });
        }
        public void Post([FromBody]PrintJob job)
        {
            queue.Enqueue(job);
        }

    }
    public class PrintJob
    {
        public string url { get; set; }
        public string html { get; set; }
        public string printer { get; set; }
    }
}

考虑到我获取一个线程来为打印队列提供服务的方式,它几乎肯定被标记为后台线程并且应该在应用程序池试图退出时终止,但我不确定这一点,所以我问亲爱的读者,感谢您在这种情况下提出的最佳实践集体观念。


好吧,我确实要求最佳实践。

然而,我没有长运行 后台任务,我有短运行 任务。它们在不同的线程上异步到达,但必须在单个线程上串行执行,因为 WinForms 打印方法是为 STA 线程设计的。

Matt Lethargic 关于可能失业的观点当然是一个考虑因素,但对于这种情况并不重要。作业永远不会排队超过几秒钟,丢失只会提示操作员重试。

就此而言,使用消息队列并不能解决 "what if someone shuts it down while it's being used" 的问题,它只是将其移至另一个软件。很多消息队列都不是持久性的,你不会相信我看到有人使用 MSMQ 解决这个问题的次数,但未能将其配置为持久性。

这非常有趣。

http://thecodelesscode.com/case/156

我会在更高层次上查看您的体系结构,执行 'long running tasks' 打印之类的操作可能应该完全在您的 webapi 进程之外进行。

如果是我们自己,我会:

创建一个包含所有打印逻辑的 windows 服务(或者你有什么),然后控制器的工作就是通过 http 或某种队列 MSMQ 与服务对话, RabbitMQ, ServiceBus 等

如果通过 http,则服务应在内部排队打印作业,并尽快(在打印发生之前)return 200/201 到控制器,以便控制器可以 return客户端高效地释放它的资源。

如果通过排队技术,那么控制器应该在队列中放置一条消息,然后再次 return 200/201 尽快,服务然后可以按照自己的速率读取消息并在一次。

这样做可以消除 api 的开销,并且还可以在网络出现故障时丢失打印作业api(如果 api 崩溃任何后台线程 may/will 受到影响)。另外,如果您在有人打印时进行部署,打印作业很可能会失败。

我的 2 美分值

我认为期望的行为不应该在 Controller 中完成。

public interface IPrintAgent {
    void Enqueue(PrintJob job);
    void Cancel();
}

以上抽象可以使用框架IDependencyResolver

实现并注入到控制器中
public class LabelController : ApiController {
    private IPrintAgent agent;

    public LabelController(IPrintAgent agent) {
        this.agent = agent;
    }

    [HttpPost]
    public IHttpActionResult Post([FromBody]PrintJob job) {
        if (ModelState.IsValid) {
            agent.Enqueue(job);
            return Ok();
        }
        return BadRequest(ModelState);
    }
}

上述场景中控制器的唯一工作是排队作业。

现在我将重点放在问题的主要部分。

正如其他人已经提到的,有很多方法可以实现所需的行为

一个简单的内存实现看起来像

public class DefaultPrintAgent : IPrintAgent {
    static readonly ConcurrentQueue<PrintJob> queue = new ConcurrentQueue<PrintJob>();
    static object syncLock = new Object();
    static bool idle = true;
    static CancellationTokenSource cts = new CancellationTokenSource();

    static DefaultPrintAgent() {
        checkQueue += OnCheckQueue;
    }

    private static event EventHandler checkQueue = delegate { };
    private static async void OnCheckQueue(object sender, EventArgs args) {
        cts = new CancellationTokenSource();
        PrintJob job = null;
        while (!queue.IsEmpty && queue.TryDequeue(out job)) {
            await Print(job);
            if (cts.IsCancellationRequested) {
               break;
            }
        }
        idle = true;
    }

    public void Enqueue(PrintJob job) {
        queue.Enqueue(job);
        if (idle) {
            lock (syncLock) {
                if (idle) {
                    idle = false;
                    checkQueue(this, EventArgs.Empty);
                }
            }
        }
    }

    public void Cancel() {
        if (!cts.IsCancellationRequested)
            cts.Cancel();
    }

    static Task Print(PrintJob job) {
        //...print job
    }
}

它利用异步事件处理程序在添加作业时按顺序处理队列。

提供Cancel以便进程可以根据需要短路。

Application_End 活动中按另一位用户的建议点赞

var agent = new DefaultPrintAgent();
agent.Cancel();

或根据需要手动公开端点。