Google Appengine上的RPC支持“等待超时”吗?

时间:2018-04-11 11:55:28

标签: google-app-engine urlfetch

我正在尝试在Google Appengine上实现以下逻辑:

rpc = call_external_service(timeout=T)
rpc.wait(timeout=T/2)
if rpc.done:
   return rpc.result
rpc2 = call_backup_service(timeout=T/2)
finished_rpc = wait_any([rpc, rpc2], timeout=T/2)
return finished_rpc.result

即,使用超时为T的服务(使用urlfetch)调用。如果未在T/2中完成,请尝试调用备份服务,然后等待其中任何一个完成

问题是RPC机制似乎没有提供“等待超时”原语。也就是说,如果我创建一个截止日期为T的RPC,我不能说“等待T / 2秒,看看RPC是否已完成”。

有没有人为此解决这个问题?

编辑:@TarunLalwani发布了一个潜在的解决方案。我们的想法是让一个特殊的处理程序睡眠一段预定的时间(如/sleep?delay=5)并将其作为第二个参数添加到UserRPC.wait_any。 IE浏览器。类似的东西:

rpc = call_external_service(timeout=T)
rpc2 = create_wait_rpc(timeout=T/2)
finished_rpc = wait_any([rpc, rpc2])
if finished_rpc == finished_rpc:
  return rpc.result
rpc2 = call_backup_service(timeout=T/2)
finished_rpc = wait_any([rpc, rpc2])
return finished_rpc.result

不幸的是,似乎UserRPC.wait_any的实现类似于以下内容:

def wait_any(rpcs):
    last_rpc = rpcs[-1]
    last_rpc.wait()
    return last_rpc

也就是说,它始终等待 last RPC完成,这在我们的案例中是个问题,因为如果初始调用在T / 2时间内完成,我们希望返回结果立即,而不是必须等待至少T / 2。我使用本地dev_appserver和生产中进行了测试(快速测试代码可以从https://github.com/cdman/gae-rpc-test中获取)。

这仍然可以通过使用rpc2的一些非常小的超时来实现,例如:

rpc = call_external_service(timeout=T)
end_time = time.time() + T/2
while time.time() < end_time:
    wait_any([rpc, create_wait_rpc(timeout=0.1)])
    if rpc.status == 2:
        return rpc.result
# else, call backup service

然而,在这里我仍然人为地将我的时间分辨率限制在100ms(所以如果初始呼叫在230ms完成,我们只在300ms后返回结果)并且我将用大量垃圾邮件发送我的日志< / em>对/sleep的请求。此外,这可能会增加运行项目的成本。

或者,如果某种类型的无操作/低开销RPC可以作为UserRPC.wait_any的第二个参数传入以保持事件循环移动,那么这个半忙等待解决方案可以也许可以工作: - )

编辑2 :我使用ndb的异步版memcache.get实现了忙等待版本。您可以在此处查看来源:https://github.com/cdman/gae-rpc-test/blob/ndb-async/main.py

理论上这应该是免费的(参见https://cloud.google.com/appengine/pricing#other-resources),但仍然感觉像是黑客。

编辑3 :看起来以下应该工作:

from google.appengine.ext.ndb import eventloop
# ...
ev = eventloop.get_event_loop()
while time.time() < end_time:
  ev.run1()
  if rpc.done():
    break
  time.sleep(0.001)

(显式运行eventloop,检查RPC并没有完成,稍微睡一会然后重试)

不幸的是,“运行eventloop”步骤只是阻塞,直到urlfetch在某一点完成:(

2 个答案:

答案 0 :(得分:4)

<强> TL; DR;

在挖掘了python appengine sdk的来源后,下面是我的观察并带走。

wait_any不是它的样子

当您在2 wait_any上使用RPCs时,您希望首先完成的那个,但同样的逻辑不是那样的

assert iter(rpcs) is not rpcs, 'rpcs must be a collection, not an iterator'
finished, running = cls.__check_one(rpcs)
if finished is not None:
  return finished
if running is None:
  return None
try:
  cls.__local.may_interrupt_wait = True
  try:
    running.__rpc.Wait()
  except apiproxy_errors.InterruptedError, err:
    err.rpc._exception = None
    err.rpc._traceback = None
finally:
  cls.__local.may_interrupt_wait = False
finished, runnning = cls.__check_one(rpcs)
return finished

在下面的代码行中

finished, running = cls.__check_one(rpcs)

方法__check_one的代码如下所示

rpc = None
for rpc in rpcs:
  assert isinstance(rpc, cls), repr(rpc)
  state = rpc.__rpc.state
  if state == apiproxy_rpc.RPC.FINISHING:
    rpc.__call_user_callback()
    return rpc, None
  assert state != apiproxy_rpc.RPC.IDLE, repr(rpc)
return None, rpc

所以它只是检查其中任何一个是否已经完成,如果没有则返回最后一个集合,最后一个return None, rpc

然后wait_any拨打running.__rpc.Wait()。因此为同一个

创建了一个简单的sleep / delay处理程序

类SleepHandler(webapp2.RequestHandler):

def get(self):
    delay = float(self.request.get('delay')) if self.request.get('delay') else 10
    sleep(delay)
    self.response.status_int = 200
    self.response.write('Response delayed by {}'.format(delay))

并在MainHandler下方添加以测试deadline

class MainHandler(webapp2.RequestHandler):
    def get(self):
        # rpc = UserRPC('dummywait', 5, stubmap=MyStubMap)

        rpc = urlfetch.create_rpc(deadline=2.0)
        rpc2 = urlfetch.create_rpc(deadline=6.0)
        urlfetch.make_fetch_call(rpc, self.request.host_url + "/sleep?delay=1")
        urlfetch.make_fetch_call(rpc2, self.request.host_url + "/sleep?delay=5")
        try:
            print(datetime.now())
            finished = apiproxy_stub_map.UserRPC.wait_any([rpc, rpc2])
            print(finished.request.url_)
            print(datetime.now())
            i = 0
        except Exception as ex:
            print_exception(ex)
        # ... do other things ...
        try:
            print(datetime.now())
            result = finished.get_result()
            print(datetime.now())
            if result.status_code == 200:
                text = result.content
                self.response.write(text)
            else:
                self.response.status_int = result.status_code
                self.response.write('URL returned status code {}'.format(
                    result.status_code))
        except urlfetch.DownloadError:
            print(datetime.now())
            self.response.status_int = 500
            self.response.write('Error fetching URL')


app = webapp2.WSGIApplication([
    ('/', MainHandler),
    ('/sleep', SleepHandler),
], debug=True)

以下是上述代码中的要点

  • rpc的截止日期为2.0秒,实际请求在1.0
  • 结束
  • rpc2的截止日期为6.0秒,实际请求在5.0
  • 结束

理想情况下,我们希望得到rpc作为完成的任务,并在url响应中显示数据。但输出是

RPC response

现在,如果我们从

切换wait_any参数的顺序
finished = apiproxy_stub_map.UserRPC.wait_any([rpc, rpc2])

finished = apiproxy_stub_map.UserRPC.wait_any([rpc2, rpc])

输出变为

以下

RPC wins

因此,这意味着如果您创建TT/2的截止日期,那么如果您将其用作最后一个参数,那么您的最短等待时间将始终为T/2

所以无论你试图在google appengine解决这个问题的解决方案,它们仍然是肮脏的技巧。现在可能的技巧是做间隔。下面是一个这样的样本

    T = 10.0
    # Deadline T
    rpc_main = urlfetch.create_rpc(deadline=T)

    # Deadline T/2
    rpc_backup = urlfetch.create_rpc(deadline=T / 2)

    urlfetch.make_fetch_call(rpc_main, self.request.host_url + "/sleep?delay=7")

    i = 0.0

    while i < T / 2:
        rpc_compare = urlfetch.create_rpc()
        urlfetch.make_fetch_call(rpc_compare, self.request.host_url + "/sleep?delay=0.5")

        finished = apiproxy_stub_map.UserRPC.wait_any([rpc_main, rpc_compare])
        i += 0.5
        if finished == rpc_main:
            break

    if finished != rpc_main:
        # we need to fire a backup request
        urlfetch.make_fetch_call(rpc_backup, self.request.host_url + "/sleep?delay=1")

        finished = apiproxy_stub_map.UserRPC.wait_any([rpc_backup, rpc_main])

    try:
        finished.get_result()
    except DeadlineExceededError as ex:
        # Rpc main finished with error then we need to switch to Backup request 
        finished = rpc_backup

在这里,我们将rpc_main作为优先级而不是备份,即使备份在这种情况下首先完成,我们也会从rpc_main获得响应

RPC Main

现在,如果我将RPC主页更改为低于截止日期

    urlfetch.make_fetch_call(rpc_main, self.request.host_url + "/sleep?delay=20")

输出将变为

RPC Backup wins

因此它显示了轮询以及最坏情况的等待场景。这是我能够查看原始源代码的唯一可能的解决方法/实现。

答案 1 :(得分:-1)

您可以使用以下urlfetch function设置自定义请求超时:

urlfetch.set_default_fetch_deadline(value)

请注意,此函数将新的默认截止日期存储在线程局部变量上,因此必须为每个请求设置它,例如,在自定义中间件中。值参数是操作的最后期限(以秒为单位);默认值是系统特定的截止日期(通常为5秒)。

实际实施将取决于您的语言,但是一旦设置了自定义超时,您就可以轻松地为您的备用服务设置最后期限/ 2的值。