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

时间:2015-12-03 21:07:02

标签: javascript python google-app-engine google-app-engine-python

原始问题

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

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

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

3 个答案:

答案 0 :(得分:4)

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

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

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

答案 1 :(得分:0)

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

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

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

答案 2 :(得分:0)

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

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

  1. 预先计算。在用户请求之前加载,计算和存储lineups值。为此,您可以使用GAE后端实例,其运行时间可超过60秒。
  2. 用户真的需要那么多数据吗?一般来说,如果有太多的数据导致计算机对其进行排序存在问题 - 那么向用户显示它已经太多了。可能你的用户只需要看到它的一小部分(比如前10名玩家,或者一些汇总统计数据)。然后改进makeLineups()中使用的算法就可以了。
  3. 推迟。如果你不能做1或2,那么你的选择是将计算推迟到Task API。为此你的前端应该:
  4. 使用任务队列排队任务:https://cloud.google.com/appengine/docs/python/taskqueue/
    • 使用渠道API向用户开放渠道:https://cloud.google.com/appengine/docs/python/channel/
    • 将该用户的channel_id保存到数据存储区。
    • 完成通话。在UI上向用户显示一条消息,例如“请等待,我们正在处理数字”。
    • 同时,GAE后端执行您入队的任务。该任务计算makeLineups()的值。完成后,任务将从数据存储区获取channel_id并向其发送计算值lineups
    • 用户前端收到该值并让用户满意。
  5. 而不是任务API,新的背景线程可能更容易和更好地适用于您的情况:https://cloud.google.com/appengine/docs/python/modules/#Python_Background_threads基本上,您调用了background_thread.BackgroundThread()而不是排队任务,其余部分保持不变。 更新这仅适用于后端模块(基本或手动缩放,而非自动)。在前端(默认)模块上,自定义线程不能超过HTTP请求,因此也限制为60秒。
  6. 如果有帮助,请告诉我。