我正在尝试在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在某一点完成:(
答案 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响应中显示数据。但输出是
现在,如果我们从
切换wait_any
参数的顺序
finished = apiproxy_stub_map.UserRPC.wait_any([rpc, rpc2])
到
finished = apiproxy_stub_map.UserRPC.wait_any([rpc2, rpc])
输出变为
以下因此,这意味着如果您创建T
和T/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主页更改为低于截止日期
urlfetch.make_fetch_call(rpc_main, self.request.host_url + "/sleep?delay=20")
输出将变为
因此它显示了轮询以及最坏情况的等待场景。这是我能够查看原始源代码的唯一可能的解决方法/实现。
答案 1 :(得分:-1)
您可以使用以下urlfetch function设置自定义请求超时:
urlfetch.set_default_fetch_deadline(value)
请注意,此函数将新的默认截止日期存储在线程局部变量上,因此必须为每个请求设置它,例如,在自定义中间件中。值参数是操作的最后期限(以秒为单位);默认值是系统特定的截止日期(通常为5秒)。
实际实施将取决于您的语言,但是一旦设置了自定义超时,您就可以轻松地为您的备用服务设置最后期限/ 2的值。