我如何 return 来自 Google App Engine 中延迟任务的数据

How do I return data from a deferred task in Google App Engine

原问题

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

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

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)

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

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

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

我是如何解决的

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

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([]))

Javascript 前端然后轮询 queryLineups URL 一段固定的时间,如果时间限制到期或接收回数据则停止轮询。我希望这对任何试图解决类似问题的人都有帮助。如果事情变得松鼠,我还有更多工作要做,让它优雅地失败,但这有效,只需要改进。

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

您的总体思路是正确的,所以我将对其进行扩展。工作流程可能如下所示:

  1. 您收到创建阵容的请求。您在数据存储中为其创建一个新实体。它应该包含一个 ID(稍后您将需要它来检索结果)和一个状态 (PENDING|DONE|FAILED)。如果对您有用,您还可以保存请求中的数据。
  2. 您推迟计算并立即 return 响应。响应将包含任务的 ID。当计算完成后,它会将任务的结果保存在数据存储中并更新任务的状态。该结果将包含任务 ID,以便我们可以轻松找到它。
  3. 前端收到ID后,开始轮询结果。使用 setTimeoutsetInterval 将带有任务 ID 的请求发送到服务器(这是一个单独的端点)。服务器检查任务的状态,如果完成则return返回结果(如果失败则出错)。
  4. 前端获取数据并停止轮询。

通常您不能再回复原始请求,因为原始请求的上下文消失了。 也许if 你从请求处理程序 return 没有回复并且 if 不知何故' 终止来自客户端的连接并且 if 你能够以某种方式保留处理程序对象,以便稍后可以在另一个(内部)请求中恢复它并使用恢复的副本回复它符合最初的要求...充其量是一种远射。

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

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

嗯,首先,让用户等待 1 分钟直到页面加载已经很糟糕了。一般来说,面向用户的 HTTP 请求应该不会超过 1 。 GAE 给出的那 60 秒——对于紧急情况来说已经太慷慨了。

我有几个建议,但不知道你的应用说出你需要什么:

  1. 预先计算。在用户请求之前加载、计算和存储 lineups 值。为此,您可以使用 GAE 后端实例,它可以 运行 超过 60 秒。
  2. 用户真的需要那么多数据吗?一般来说,如果有太多数据以至于计算机无法对其进行排序——它已经太多了,无法向用户显示。可能您的用户只需要查看其中的一小部分(例如前 10 名玩家,或一些汇总统计数据)。然后改进 makeLineups() 中使用的算法就可以解决问题。
  3. 推迟。如果您不能执行 1 或 2,那么您的选择是将计算推迟到任务 API。为此,您的前端应该:
  4. 使用任务队列排队任务:https://cloud.google.com/appengine/docs/python/taskqueue/
    • 使用频道 API 向用户开放频道:https://cloud.google.com/appengine/docs/python/channel/
    • 将该用户的 channel_id 保存到数据存储。
    • 结束通话。在 UI 上向用户显示一条消息,例如 "please wait, we're crunching down the numbers"。
    • 同时,GAE后端执行您入队的任务。该任务计算 makeLineups() 的值。完成后,该任务将从 Datastore 获取 channel_id 并将 lineups.
    • 的计算值发送到那里
    • 用户前端收到价值并让用户开心。
  5. 代替任务 API 有新的后台线程,它可能更适合您的情况:https://cloud.google.com/appengine/docs/python/modules/#Python_Background_threads 基本上,您调用 background_thread.BackgroundThread(),而不是将任务排入队列,其余的保持不变。 更新 这仅适用于后端模块(基本或手动缩放,而不是自动缩放)。在前端(默认)模块上,自定义线程不能超过 HTTP 请求,因此也限制为 60 秒。

如果有帮助请告诉我。