带队列的长时间运行的 REST API

我们正在实现一个 REST API,它将启动多个长时间运行的后端任务。我一直在阅读 RESTful Web Services Cookbook,建议返回 HTTP 202/Accepted,其中 Content-Location 头部指向正在处理的任务。(例如 http://www.example.org/orders/tasks/1234) ,并让客户端轮询这个 URI 以获得长时间运行任务的更新。

其思想是让 REST API 立即将消息发布到队列中,后台工作者角色从队列中接收消息并旋转多个后端任务(也使用队列)。我在这种方法中看到的问题是如何为任务分配一个唯一的 ID,然后让客户机通过向 Content-Location URI 发出 GET 来请求任务的状态。

如果 REST API 立即发送到一个队列,那么它可以生成一个 GUID,并将其作为添加到队列中的消息的属性附加到队列中,但是获取请求的状态变得非常困难。

另一种选择是让 REST API 立即向数据库添加一个条目(比方说一个订单,带有一个新的订单 id) ,带有一个初始状态,然后在队列中放入一条消息,启动后台任务,后台任务随后更新该数据库记录。API 将在 Content-Location 头的 URI 中返回这个新的订单 ID,供客户机在检查任务状态时使用。

以某种方式首先添加数据库条目,然后将消息添加到队列似乎是向后的,但是只将请求添加到队列会使跟踪进度变得困难。

建议的方法是什么?

非常感谢你的洞察力。

48305 次浏览

我猜你的系统是这样的。您有一个 REST 服务,它接收来自客户端的请求。它将请求转换为业务逻辑可以理解的命令。将这些命令放入一个队列中。您有一个或多个工作者,它们可以处理并从队列中删除这些命令,并将结果发送给 REST 服务,后者可以响应客户机。

您的问题是,由于您的长时间运行任务导致客户端连接超时,因此无法发送响应。因此,您可以在将命令放入队列并添加轮询链接之后,发送一个202接受,这样客户机将能够轮询更改。您的任务有多个子任务,因此存在进展,而不仅仅是挂起和完成状态更改。

  1. 如果希望坚持轮询,应该创建一个新的 REST 资源,其中包含长时间运行任务的实际状态和进度。这意味着您必须将这些信息存储在数据库中,以便 REST 服务能够响应像 GET /tasks/23461/status这样的请求。这意味着您的工作人员必须在完成子任务或整个任务时更新数据库。
  2. 如果您的 REST 服务作为守护进程运行,那么您可以通过进度通知它,因此在数据库中存储任务状态不是工作者的责任。这种 REST 服务也可以将信息存储在内存中。
  3. 如果您决定使用 websockets 通知客户端,那么您可以创建一个通知服务。通过 REST,您必须使用任务 ID 进行响应。之后,您在 websocket 连接上发送回这个任务 ID,这样通知服务将知道哪个 websocket 连接订阅了某个任务的事件。在此之后,您就不需要 REST 服务了,只要客户端不关闭连接,您就可以通过 websocket 连接发送进度。
  4. 您可以按照以下方式组合这些解决方案。您让 REST 服务创建任务资源,这样就可以通过使用轮询链接访问进度。之后,您发回一个带有202的标识符,该标识符通过 websockets 连接发回。因此,您可以使用通知服务来通知客户端。按照进度,您的工作人员将通知 REST 服务,该服务将创建一个类似于 GET /tasks/23461/status的链接,并通过通知服务将该链接发送给客户机。之后,客户端可以使用该链接来更新其状态。

我认为最后一个是最好的解决方案,如果您的 REST 服务作为守护进程运行。这是因为您可以将通知责任转移到一个专用的通知服务,该服务可以使用 websockets、轮询、 SSE 等任何您想要的服务。它可以在不关闭 REST 服务的情况下崩溃,因此 REST 服务将保持稳定和快速。如果您发送回来的手动更新链接也与202,然后客户端可以进行手动更新(假设一个人类控制的客户端) ,所以您将有一些像优雅的退化,如果通知服务是不可用的。您不必维护通知服务,因为它不知道任务的任何信息,它只是向客户机发送数据。您的工作人员不必知道如何发送通知和如何创建超链接。维护客户机代码也会更容易,因为它几乎是一个纯 REST 客户机。唯一的额外特性将是通知链接的订阅,它不会经常更改。