如何从Google App Engine中的延迟任务返回数据

时间:2020-11-27 02:16:50

Original Question

I have a working version of my web application that I am trying to upgrade at the moment, and I'm running into the issue of having a task which takes too long to complete in during a single HTTP request. The application takes a JSON list from a JavaScript front end by an HTTP Post operation, and returns a sorted/sliced version of that list. As the input list gets longer, the sorting operation takes a much longer time to perform (obviously), so on suitably long input lists, I hit the 60 second HTTP request timeout, and the application fails.

我有一个我正在尝试升级的Web应用程序的工作版本,并且我遇到了在单个HTTP请求期间完成任务需要很长时间才能完成的问题。应用程序通过HTTP Post操作从JavaScript前端获取JSON列表,并返回该列表的排序/切片版本。随着输入列表变得越来越长,排序操作需要更长的时间来执行(显然),因此在适当长的输入列表上,我达到60秒的HTTP请求超时,并且应用程序失败。

I would like to start using the deferred library to perform the sort task, but I'm not clear on how to store/retrieve the data after I perform that task. Here is my current code:

我想开始使用延迟库来执行排序任务,但我不清楚在执行该任务后如何存储/检索数据。这是我目前的代码:

class getLineups(webapp2.RequestHandler):
  def post(self):
    jsonstring = self.request.body
    inputData = json.loads(jsonstring)
    playerList = inputData["pList"]
    positions = ["QB","RB","WR","TE","DST"]

    playersPos = sortByPos(playerList,positions)
    rosters, playerUse = getNFLRosters(playersPos, positions)
    try:
      # This step is computationally expensive, it will fail on large player lists.
      lineups = makeLineups(rosters,playerUse,50000)

      self.response.headers["Content-Type"] = "application/json"
      self.response.out.write(json.dumps(lineups))
    except:
      logging.error("60 second timeout reached on player list of length:", len(playerList))
      self.response.headers["Content-Type"] = "text/plain"
      self.response.set_status(504)

app = webapp2.WSGIApplication([
  ('/lineup',getLineups),
], debug = True)

Ideally I would like to replace the entire try/except block with a call to the deferred task library:

理想情况下,我想通过调用延迟任务库来替换整个try / except块:

deferred.defer(makeLineups,rosters,playerUse,50000)

But I'm unclear on how I would get the result back from that operation. I'm thinking I would have to store it in the Datastore, and then retrieve it, but how would my JavaScript front end know when the operation is complete? I've read the documentation on Google's site, but I'm still hazy on how to accomplish this task.

但我不清楚我将如何从该操作中获得结果。我想我必须将它存储在数据存储区中,然后检索它,但我的JavaScript前端如何知道操作何时完成?我已经阅读了Google网站上的文档,但我仍然对如何完成此任务感到茫然。

How I Solved It

Using the basic outline in the accepted answer, here's how I solved this problem:

在接受的答案中使用基本大纲,这是我如何解决这个问题:

def solveResult(result_key):
  result = result_key.get()

  playersPos = sortByPos(result.playerList, result.positions)
  rosters, playerUse = getNFLRosters(playersPos,result.positions)

  lineups = makeLineups(rosters,playerUse,50000)
  storeResult(result_key,lineups)

@ndb.transactional
def storeResult(result_key,lineups):
  result = result_key.get()
  result.lineups = lineups
  result.solveComplete = True
  result.put()

class Result(ndb.Model):
  playerList = ndb.JsonProperty()
  positions = ndb.JsonProperty()
  solveComplete = ndb.BooleanProperty()

class getLineups(webapp2.RequestHandler):
  def post(self):
    jsonstring = self.request.body
    inputData = json.loads(jsonstring)

    deferredResult = Result(
      playerList = inputData["pList"],
      positions = ["QB","RB","WR","TE","DST"],
      solveComplete = False
    )

    deferredResult_key = deferredResult.put()

    deferred.defer(solveResult,deferredResult_key)

    self.response.headers["Content-Type"] = "text/plain"
    self.response.out.write(deferredResult_key.urlsafe())

class queryResults(webapp2.RequestHandler):
  def post(self):
    safe_result_key = self.request.body
    result_key = ndb.Key(urlsafe=safe_result_key)

    result = result_key.get()
    self.response.headers["Content-Type"] = "application/json"

    if result.solveComplete:
      self.response.out.write(json.dumps(result.lineups))
    else:
      self.response.out.write(json.dumps([]))

The Javascript frontend then polls queryLineups URL for a fixed amount of time and stops polling if either the time limit expires, or it receives data back. I hope this is helpful for anyone else attempting to solve a similar problem. I have a bit more work to do to make it fail gracefully if things get squirrelly, but this works and just needs refinement.

然后Javascript前端轮询queryLineups URL一段固定的时间,并在时间限制到期或者接收数据时停止轮询。我希望这对试图解决类似问题的其他人有帮助。如果事情变得松散,我还有一些工作要做,以使它优雅地失败,但这是有效的,只需要改进。

3 个解决方案

#1


4  

I'm not familiar with GAE, but this is a fairly generic question, so I can give you some advice.

我不熟悉GAE,但这是一个相当普遍的问题,所以我可以给你一些建议。

Your general idea is correct, so I'm just going to expand on it. The workflow could look like this:

你的总体想法是正确的,所以我只是要扩展它。工作流程可能如下所示:

  1. You get the request to create the lineups. You create a new entity in the datastore for it. It should contain an ID (you'll need it to retrieve the result later) and a status (PENDING|DONE|FAILED). You can also save the data from the request, if that's useful to you.
  2. 您收到了创建阵容的请求。您可以在数据存储区中为其创建新实体。它应该包含一个ID(您需要它以便稍后检索结果)和一个状态(PENDING | DONE | FAILED)。您也可以保存请求中的数据(如果这对您有用)。
  3. You defer the computation and return a response right away. The response will contain the ID of the task. When the computation is done, it will save the result of the task in the Datastore and update the status of the task. That result will contain the task ID, so that we can easily find it.
  4. 您推迟计算并立即返回响应。响应将包含任务的ID。计算完成后,它会将任务结果保存在数据存储区中并更新任务的状态。该结果将包含任务ID,以便我们可以轻松找到它。
  5. Once the frontend receives the ID, it starts polling for the result. Using setTimeout or setInterval you send requests with the task ID to the server (this is a separate endpoint). The server checks the status of the task, and returns the result if it's done (error if failed).
  6. 一旦前端收到ID,它就开始轮询结果。使用setTimeout或setInterval将带有任务ID的请求发送到服务器(这是一个单独的端点)。服务器检查任务的状态,如果完成则返回结果(如果失败则返回错误)。
  7. The frontend gets the data and stops polling.
  8. 前端获取数据并停止轮询。

#2


0  

Normally you can't reply to the original request anymore since the context of that original request dissapears. Maybe, if you return from the request handler without replying and if somehow that doesn't kill the connection from the client and if you are somehow able to persist the handler object so that you can later restore it in another (internal) request and use the restored copy to reply from it to the original request... Kind of a long shot at best.

通常,由于原始请求的上下文消失,您无法再回复原始请求。也许,如果你从请求处理程序返回而没有回复,并且如果某种方式不会从客户端终止连接,并且如果你以某种方式能够持久化处理程序对象,以便以后可以在另一个(内部)请求中恢复它并使用恢复的副本从它回复原始请求......最好是一种远射。

One option would be to split the operation into a sequence: - a 1st request starting the operation - subsequent one or more polling requests until the operation completes and the result is available

一种选择是将操作拆分为一个序列: - 第一个请求开始操作 - 后续一个或多个轮询请求,直到操作完成并且结果可用

Another approach may be possible if the expensive operation is mainly executing on data available prior to when the operation is invoked. You could re-org the app logic so that partial results are computed as soon as the respective data becomes available, so that when the final operation is requested it only operates on pre-computed partial results. An analogy, if you want, would be Google search requests immediately receiving replies with data from pre-computed indexes instead of waiting for an actual web search to be performed.

如果昂贵的操作主要是在调用操作之前可用的数据上执行,则另一种方法是可能的。您可以重新组织应用程序逻辑,以便在相应数据可用时立即计算部分结果,以便在请求最终操作时,它仅对预先计算的部分结果进行操作。如果您愿意,可以通过类比,谷歌搜索请求立即接收来自预先计算的索引的数据的回复,而不是等待实际的网络搜索。

#3


0  

Well, first, it's already bad to let users wait for 1 minute until page loads. In general, user-facing HTTP requests should take no more than 1 second. Those 60 seconds that GAE gives -- is already too generous, for critical situations.

好吧,首先,让用户等待1分钟直到页面加载已经很糟糕了。通常,面向用户的HTTP请求不应超过1秒。 GAE给出的60秒 - 对于危急情况来说已经过于慷慨了。

I have several suggestions, but I don't know your application to say what you need:

我有几个建议,但我不知道你的申请表明你需要什么:

  1. Precompute. Load, compute and store lineups value before user request it. For that you can utilize GAE Backend instances, which can run way longer than 60 seconds.
  2. 预先计算。在用户请求之前加载,计算和存储阵容值。为此,您可以使用GAE后端实例,其运行时间超过60秒。
  3. Do users really need that much data? Generally, if there's so much data that computer has problems sorting it -- it's already too much to show to user. Probably your users just need to see some small part of it (like top 10 players, or some aggregate statistics). Then improvement of algorithm used in makeLineups() will do the trick.
  4. 用户真的需要那么多数据吗?一般来说,如果有太多的数据导致计算机在排序时出现问题 - 那么向用户显示它已经太多了。可能你的用户只需要看到它的一小部分(比如前10名玩家,或者一些汇总统计数据)。然后改进makeLineups()中使用的算法就可以了。
  5. Defer. If you cannot do 1 or 2, then your option is to defer the computation to Task API. For that your frontend should:
  6. 推迟。如果你不能做1或2,那么你的选择是将计算推迟到Task API。为此你的前端应该:
  7. Enqueue a task using Task Queue: https://cloud.google.com/appengine/docs/python/taskqueue/
    • Open channel to user using Channel API: https://cloud.google.com/appengine/docs/python/channel/
    • 使用渠道API向用户开放渠道:https://cloud.google.com/appengine/docs/python/channel/
    • Save the channel_id for that user to Datastore.
    • 将该用户的channel_id保存到数据存储区。
    • Finish the call. On UI show user a message like "please wait, we're crunching down the numbers".
    • 完成通话。在UI上显示用户一条消息,如“请等待,我们正在处理数字”。
    • At the same time, GAE backend executes the task you enqueued. The task computes value of makeLineups(). Once done, the task will take channel_id from Datastore and send there the computed value of lineups.
    • 同时,GAE后端执行您入队的任务。该任务计算makeLineups()的值。完成后,任务将从Datastore获取channel_id并向其发送计算的阵容值。
    • User frontend receives the value and makes user happy.
    • 用户前端接收该值并使用户满意。
  8. 使用任务队列排队任务:https://cloud.google.com/appengine/docs/python/taskqueue/使用渠道API向用户开放渠道:https://cloud.google.com/appengine/docs/python/channel /将该用户的channel_id保存到数据存储区。完成通话。在UI上显示用户一条消息,如“请等待,我们正在处理数字”。同时,GAE后端执行您入队的任务。该任务计算makeLineups()的值。完成后,任务将从Datastore获取channel_id并向其发送计算的阵容值。用户前端接收该值并使用户满意。
  9. Instead of Task API there's new Background Threads that may be easier and better for your case: https://cloud.google.com/appengine/docs/python/modules/#Python_Background_threads Basically, instead of enqueueing a task, you call'd background_thread.BackgroundThread(), the rest stays the same. UPDATE This will work better only with backend modules (basic or manual scaling, not automatic). On Frontend (default) modules, custom threads cannot outlive HTTP request, and hence also limited to 60s.
  10. 而不是任务API,新的背景线程可能更容易和更好地适用于您的情况:https://cloud.google.com/appengine/docs/python/modules/#Python_Background_threads基本上,你打电话给,而不是排队任务background_thread.BackgroundThread(),其余部分保持不变。更新这仅适用于后端模块(基本或手动缩放,而非自动)。在前端(默认)模块上,自定义线程不能超过HTTP请求,因此也限制为60秒。

Let me know if that helps.

如果有帮助,请告诉我。

#1


4  

I'm not familiar with GAE, but this is a fairly generic question, so I can give you some advice.

我不熟悉GAE,但这是一个相当普遍的问题,所以我可以给你一些建议。

Your general idea is correct, so I'm just going to expand on it. The workflow could look like this:

你的总体想法是正确的,所以我只是要扩展它。工作流程可能如下所示:

  1. You get the request to create the lineups. You create a new entity in the datastore for it. It should contain an ID (you'll need it to retrieve the result later) and a status (PENDING|DONE|FAILED). You can also save the data from the request, if that's useful to you.
  2. 您收到了创建阵容的请求。您可以在数据存储区中为其创建新实体。它应该包含一个ID(您需要它以便稍后检索结果)和一个状态(PENDING | DONE | FAILED)。您也可以保存请求中的数据(如果这对您有用)。
  3. You defer the computation and return a response right away. The response will contain the ID of the task. When the computation is done, it will save the result of the task in the Datastore and update the status of the task. That result will contain the task ID, so that we can easily find it.
  4. 您推迟计算并立即返回响应。响应将包含任务的ID。计算完成后,它会将任务结果保存在数据存储区中并更新任务的状态。该结果将包含任务ID,以便我们可以轻松找到它。
  5. Once the frontend receives the ID, it starts polling for the result. Using setTimeout or setInterval you send requests with the task ID to the server (this is a separate endpoint). The server checks the status of the task, and returns the result if it's done (error if failed).
  6. 一旦前端收到ID,它就开始轮询结果。使用setTimeout或setInterval将带有任务ID的请求发送到服务器(这是一个单独的端点)。服务器检查任务的状态,如果完成则返回结果(如果失败则返回错误)。
  7. The frontend gets the data and stops polling.
  8. 前端获取数据并停止轮询。

#2


0  

Normally you can't reply to the original request anymore since the context of that original request dissapears. Maybe, if you return from the request handler without replying and if somehow that doesn't kill the connection from the client and if you are somehow able to persist the handler object so that you can later restore it in another (internal) request and use the restored copy to reply from it to the original request... Kind of a long shot at best.

通常,由于原始请求的上下文消失,您无法再回复原始请求。也许,如果你从请求处理程序返回而没有回复,并且如果某种方式不会从客户端终止连接,并且如果你以某种方式能够持久化处理程序对象,以便以后可以在另一个(内部)请求中恢复它并使用恢复的副本从它回复原始请求......最好是一种远射。

One option would be to split the operation into a sequence: - a 1st request starting the operation - subsequent one or more polling requests until the operation completes and the result is available

一种选择是将操作拆分为一个序列: - 第一个请求开始操作 - 后续一个或多个轮询请求,直到操作完成并且结果可用

Another approach may be possible if the expensive operation is mainly executing on data available prior to when the operation is invoked. You could re-org the app logic so that partial results are computed as soon as the respective data becomes available, so that when the final operation is requested it only operates on pre-computed partial results. An analogy, if you want, would be Google search requests immediately receiving replies with data from pre-computed indexes instead of waiting for an actual web search to be performed.

如果昂贵的操作主要是在调用操作之前可用的数据上执行,则另一种方法是可能的。您可以重新组织应用程序逻辑,以便在相应数据可用时立即计算部分结果,以便在请求最终操作时,它仅对预先计算的部分结果进行操作。如果您愿意,可以通过类比,谷歌搜索请求立即接收来自预先计算的索引的数据的回复,而不是等待实际的网络搜索。

#3


0  

Well, first, it's already bad to let users wait for 1 minute until page loads. In general, user-facing HTTP requests should take no more than 1 second. Those 60 seconds that GAE gives -- is already too generous, for critical situations.

好吧,首先,让用户等待1分钟直到页面加载已经很糟糕了。通常,面向用户的HTTP请求不应超过1秒。 GAE给出的60秒 - 对于危急情况来说已经过于慷慨了。

I have several suggestions, but I don't know your application to say what you need:

我有几个建议,但我不知道你的申请表明你需要什么:

  1. Precompute. Load, compute and store lineups value before user request it. For that you can utilize GAE Backend instances, which can run way longer than 60 seconds.
  2. 预先计算。在用户请求之前加载,计算和存储阵容值。为此,您可以使用GAE后端实例,其运行时间超过60秒。
  3. Do users really need that much data? Generally, if there's so much data that computer has problems sorting it -- it's already too much to show to user. Probably your users just need to see some small part of it (like top 10 players, or some aggregate statistics). Then improvement of algorithm used in makeLineups() will do the trick.
  4. 用户真的需要那么多数据吗?一般来说,如果有太多的数据导致计算机在排序时出现问题 - 那么向用户显示它已经太多了。可能你的用户只需要看到它的一小部分(比如前10名玩家,或者一些汇总统计数据)。然后改进makeLineups()中使用的算法就可以了。
  5. Defer. If you cannot do 1 or 2, then your option is to defer the computation to Task API. For that your frontend should:
  6. 推迟。如果你不能做1或2,那么你的选择是将计算推迟到Task API。为此你的前端应该:
  7. Enqueue a task using Task Queue: https://cloud.google.com/appengine/docs/python/taskqueue/
    • Open channel to user using Channel API: https://cloud.google.com/appengine/docs/python/channel/
    • 使用渠道API向用户开放渠道:https://cloud.google.com/appengine/docs/python/channel/
    • Save the channel_id for that user to Datastore.
    • 将该用户的channel_id保存到数据存储区。
    • Finish the call. On UI show user a message like "please wait, we're crunching down the numbers".
    • 完成通话。在UI上显示用户一条消息,如“请等待,我们正在处理数字”。
    • At the same time, GAE backend executes the task you enqueued. The task computes value of makeLineups(). Once done, the task will take channel_id from Datastore and send there the computed value of lineups.
    • 同时,GAE后端执行您入队的任务。该任务计算makeLineups()的值。完成后,任务将从Datastore获取channel_id并向其发送计算的阵容值。
    • User frontend receives the value and makes user happy.
    • 用户前端接收该值并使用户满意。
  8. 使用任务队列排队任务:https://cloud.google.com/appengine/docs/python/taskqueue/使用渠道API向用户开放渠道:https://cloud.google.com/appengine/docs/python/channel /将该用户的channel_id保存到数据存储区。完成通话。在UI上显示用户一条消息,如“请等待,我们正在处理数字”。同时,GAE后端执行您入队的任务。该任务计算makeLineups()的值。完成后,任务将从Datastore获取channel_id并向其发送计算的阵容值。用户前端接收该值并使用户满意。
  9. Instead of Task API there's new Background Threads that may be easier and better for your case: https://cloud.google.com/appengine/docs/python/modules/#Python_Background_threads Basically, instead of enqueueing a task, you call'd background_thread.BackgroundThread(), the rest stays the same. UPDATE This will work better only with backend modules (basic or manual scaling, not automatic). On Frontend (default) modules, custom threads cannot outlive HTTP request, and hence also limited to 60s.
  10. 而不是任务API,新的背景线程可能更容易和更好地适用于您的情况:https://cloud.google.com/appengine/docs/python/modules/#Python_Background_threads基本上,你打电话给,而不是排队任务background_thread.BackgroundThread(),其余部分保持不变。更新这仅适用于后端模块(基本或手动缩放,而非自动)。在前端(默认)模块上,自定义线程不能超过HTTP请求,因此也限制为60秒。

Let me know if that helps.

如果有帮助,请告诉我。