Twisted延迟与Web服务中的阻塞
我在尝试让使用Deferred对象的网络服务代码表现得和不使用它的代码一样,但遇到了困难。我的目标是写一个装饰器,可以把任何方法的处理(与Twisted无关)委托给Twisted的线程池,这样就不会阻塞反应器,同时又不改变这个方法的本质。
当下面的echo类的实例作为网络服务暴露出来时,这段代码:
from twisted.web import server, resource
from twisted.internet import defer, threads
from cgi import escape
from itertools import count
class echo(resource.Resource):
isLeaf = True
def errback(self, failure): return failure
def callback1(self, request, value):
#raise ValueError # E1
lines = ['<html><body>\n',
'<p>Page view #%s in this session</p>\n' % (value,),
'</body></html>\n']
return ''.join(lines)
def callback2(self, request, encoding):
def execute(message):
#raise ValueError # E2
request.write(message.encode(encoding))
#raise ValueError # E3
request.finish()
#raise ValueError # E4
return server.NOT_DONE_YET
return execute
def render_GET(self, request):
content_type, encoding = 'text/html', 'UTF-8'
request.setHeader('Content-Type', '%s; charset=%s' %
tuple(map(str, (content_type, encoding))))
s = request.getSession()
if not hasattr(s, 'counter'):
s.counter = count(1)
d = threads.deferToThread(self.callback1, request, s.counter.next())
d.addCallback(self.callback2(request, encoding))
d.addErrback(self.errback)
#raise ValueError # E5
return server.NOT_DONE_YET
如果把所有的raise语句注释掉,浏览器会显示一个HTML文档;如果包含标记为"E5"的raise语句,浏览器会显示一个格式良好的堆栈跟踪(这是Twisted自动为我做的)。这正是我想要的。同样,如果我完全不使用Deferred对象,把callback1和callback2的所有行为放在render_GET()里,那么在render_GET()的任何地方抛出的异常都会产生我想要的堆栈跟踪。
我想写的代码是能够立即响应浏览器请求,不在Twisted内部造成资源泄漏,并且在包含任何“E1”到“E3”的raise语句的情况下,也能在浏览器中显示堆栈跟踪——当然我知道堆栈跟踪本身会有所不同。(至于“E4”的情况,我就没那么在意了。)在阅读了Twisted的文档和这个网站上的其他问题后,我不确定该如何实现这一点。我原以为添加一个errback会有所帮助,但显然并没有。我觉得我对Deferred对象和twisted.web的堆栈之间的关系还不太理解。
我在这里记录的日志效果可能受到我使用PythonLoggingObserver将Twisted日志与标准日志模块连接的影响。
当包含“E1”时,浏览器会等到反应器关闭,此时ValueError异常和堆栈跟踪被记录,浏览器收到的是一个空文档。
当包含“E2”时,ValueError异常和堆栈跟踪会立即被记录,但浏览器仍然会等到反应器关闭,最终收到一个空文档。
当包含“E3”时,ValueError异常和堆栈跟踪会立即被记录,浏览器会等到反应器关闭,然后收到预期的文档。
当包含“E4”时,预期的文档会立即返回给浏览器,同时ValueError异常和堆栈跟踪也会立即被记录。(这种情况下会有资源泄漏的可能吗?)
2 个回答
我通过查看Twisted的源代码搞明白了这一点。关键的理解是,反应器(reactor)和Deferred的回调/错误回调逻辑是和请求对象分开的,而请求对象是用来把数据传回浏览器的。错误回调是必须的,但不能像我之前发的代码那样简单地把错误对象传递下去。错误回调必须把错误报告给浏览器。
下面的代码满足了我的需求(不会让浏览器等待,总是提供堆栈跟踪,不需要重启反应器就能继续工作),并且可以让我装饰阻塞的方法,从而把它们委派给线程,这样反应器就能对其他事件保持响应(这些方法基本上会替代这里的callback1)。不过,我发现如果在下面的代码中取消注释“E4”这个抛出语句,会导致后续的浏览器请求出现非常奇怪的行为(之前请求的部分数据被返回给浏览器;死锁)。
希望其他人能觉得这个Deferred的例子有用。
from twisted.web import server, resource
from twisted.internet import defer, threads
from itertools import count
class echo(resource.Resource):
isLeaf = True
def errback(self, request):
def execute(failure):
request.processingFailed(failure)
return failure
return execute
def callback1(self, value):
#raise ValueError # E1
lines = ['<html><body>\n',
'<p>Page view #%s in this session</p>\n' % (value,),
'</body></html>\n']
return ''.join(lines)
def callback2(self, request, encoding):
def execute(message):
#raise ValueError # E2
request.write(message.encode(encoding))
#raise ValueError # E3
request.finish()
#raise ValueError # E4
return server.NOT_DONE_YET
return execute
def render_GET(self, request):
content_type, encoding = 'text/html', 'UTF-8'
request.setHeader('Content-Type', '%s; charset=%s' %
tuple(map(str, (content_type, encoding))))
s = request.getSession()
if not hasattr(s, 'counter'):
s.counter = count(1)
d = threads.deferToThread(self.callback1, s.counter.next())
eback = self.errback(request)
d.addErrback(eback)
d.addCallback(self.callback2(request, encoding))
d.addErrback(eback)
#raise ValueError # E5
return server.NOT_DONE_YET
好的,经过几次阅读你的问题,我觉得我明白你在问什么了。我也对你的代码进行了改进,让它比你原来的答案更好一些。这个新答案应该能展示出deferred的所有强大功能。
from twisted.web import server, resource
from twisted.internet import defer, threads
from itertools import count
class echo(resource.Resource):
isLeaf = True
def errback(self, failure, request):
failure.printTraceback() # This will print the trace back in a way that looks like a python exception.
# log.err(failure) # This will use the twisted logger. This is the best method, but
# you need to import twisted log.
request.processingFailed(failure) # This will send a trace to the browser and close the request.
return None # We have dealt with the failure. Clean it out now.
def final(self, message, request, encoding):
# Message will contain the message returned by callback1
request.write(message.encode(encoding)) # This will write the message and return it to the browser.
request.finish() # Done
def callback1(self, value):
#raise ValueError # E1
lines = ['<html><body>\n',
'<p>Page view #%s in this session</p>\n' % (value,),
'</body></html>\n']
return ''.join(lines)
#raise ValueError # E4
def render_GET(self, request):
content_type, encoding = 'text/html', 'UTF-8'
request.setHeader('Content-Type', '%s; charset=%s' %
tuple(map(str, (content_type, encoding))))
s = request.getSession()
if not hasattr(s, 'counter'):
s.counter = count(1)
d = threads.deferToThread(self.callback1, s.counter.next())
d.addCallback(self.final, request, encoding)
d.addErrback(self.errback, request) # We put this here in case the encoding raised an exception.
#raise ValueError # E5
return server.NOT_DONE_YET
另外,我建议你去看看krondo的教程。它会教你关于deferred的所有知识。
编辑:
我修改了上面的代码,修复了一些小错误,也进行了改进。如果在任何地方发生异常(除了在self.errback
中,不过我们需要一定的信任),那么这个异常会被传递到self.errback
,它会在twisted中记录或打印错误,然后将错误信息发送到浏览器并且关闭请求。这应该能防止任何资源泄漏。