Twisted延迟与Web服务中的阻塞

8 投票
2 回答
3308 浏览
提问于 2025-04-16 17:05

我在尝试让使用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 个回答

1

我通过查看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
4

好的,经过几次阅读你的问题,我觉得我明白你在问什么了。我也对你的代码进行了改进,让它比你原来的答案更好一些。这个新答案应该能展示出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中记录或打印错误,然后将错误信息发送到浏览器并且关闭请求。这应该能防止任何资源泄漏。

撰写回答