Twisted deferred vs blocking в веб-сервисах - PullRequest
8 голосов
/ 06 мая 2011

Я изо всех сил пытаюсь создать такое же поведение в коде веб-службы, в котором используются отложенные объекты, и в коде, который этого не делает. Моя цель - написать декоратор, который делегирует обработку любого метода (который отделен от 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

будет отображать HTML-документ в браузере, когда все операторы повышения закомментированы, и отображать хорошо отформатированную трассировку стека (что делает для меня Twisted), когда включен оператор повышения, помеченный как «E5». Это то, что я хочу. Аналогично, если я вообще не использую отложенные объекты и помещаю все поведение из callback1 и callback2 в render_GET (), исключение, возбуждаемое в любом месте render_GET, приведет к желаемой трассировке стека.

Я пытаюсь написать код, который будет незамедлительно реагировать на браузер, не вызывать утечку ресурсов в Twisted и приводить к тому, что трассировка стека браузера также будет отображаться в случаях, когда любой из операторов повышения от "E1" до "E3" включен в отложенный код - хотя, конечно, я понимаю, что сами трассировки стека будут другими. (Дело «E4» меня не особо волнует.) После прочтения документации Twisted и других вопросов на этом сайте я не уверен, как этого добиться. Я бы подумал, что добавление errback должно облегчить это, но, очевидно, нет. Должно быть что-то с отложенными объектами и стеком twisted.web, чего я не понимаю.

Влияние на ведение журнала, которое я здесь описываю, может зависеть от того, как я использовал PythonLoggingObserver для привязки Twisted logging к стандартному модулю ведения журнала.

Когда включено «E1», браузер ожидает, пока реактор не будет остановлен, после чего регистрируется исключение ValueError с трассировкой стека, и браузер получает пустой документ.

Когда включено «E2», исключение ValueError с трассировкой стека регистрируется немедленно, но браузер ждет, пока реактор не отключится, и в этот момент он получит пустой документ.

Когда включено «E3», исключение ValueError с трассировкой стека регистрируется немедленно, браузер ждет, пока реактор выключится, и в этот момент получает нужный документ.

Если включено выражение «E4», намеченный документ немедленно возвращается в браузер, и немедленно регистрируется исключение ValueError со трассировкой стека. (Есть ли вероятность утечки ресурса в этом случае?)

Ответы [ 2 ]

4 голосов
/ 06 мая 2011

Хорошо, прочитав ваш вопрос несколько раз, я думаю, что понимаю, о чем вы спрашиваете.Я также переработал ваш код, чтобы сделать его немного лучше, чем ваш первоначальный ответ.Этот новый ответ должен показать все возможности 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 .Он научит вас всему, что вам нужно знать об отложенном.

Редактировать:

Измените код выше, чтобы исправить некоторые глупые ошибки.Также улучшил это.Если исключение случается где-либо (кроме self.errback, но нам нужен некоторый уровень доверия), то оно будет передано self.errback, который зарегистрирует или напечатает ошибку в витой форме, а затем отправит трассировку в браузер закрыть запрос.Это должно остановить любые утечки ресурсов.

1 голос
/ 06 мая 2011

Я понял это, покопавшись в скрученном источнике. Необходимая информация заключается в том, что логика реактора и цепочки отложенного обратного / обратного вызова отделена от объекта запроса, и именно так данные возвращаются в браузер. Ошибка необходима, но она не может просто распространять объект Failure по цепочке, как в исходном коде, который я разместил. Ошибка должна сообщить об ошибке браузеру.

Приведенный ниже код отвечает моим требованиям (никогда не заставляет браузер ждать, всегда выдает трассировку стека, не требует перезапуска реактора для возобновления работы) и позволит мне декорировать методы блокировки и тем самым делегировать их потокам для сохранения реактор, реагирующий на другие события (такие методы по существу займут здесь callback1). Тем не менее, я обнаружил, что в приведенном ниже коде раскомментирование оператора R4 «E4» вызывает очень странное поведение при последующих запросах браузера (частичные данные из предыдущих запросов возвращаются в браузер; взаимоблокировка).

Надеюсь, другие найдут это полезным отложенным примером.

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
...