Upgrade to Pro — share decks privately, control downloads, hide ads and more …

The Report of Twisted's Death; or Twisted & Tor...

The Report of Twisted's Death; or Twisted & Tornado in the Asyncio Age (Perth Django Meetup)

Given at the Perth Django Meetup

Amber Brown (HawkOwl)

May 05, 2016
Tweet

More Decks by Amber Brown (HawkOwl)

Other Decks in Technology

Transcript

  1. This is an alpha of a talk to be given

    at PyCon US 2016 Please excuse any mistakes or formatting weirdness :)
  2. Selector functions take a list of file descriptors (e.g. sockets,

    open files) and tell you what is ready for reading or writing
  3. import asyncio import datetime async def display_date(loop): end_time = loop.time()

    + 5.0 while True: print(datetime.datetime.now()) if (loop.time() + 1.0) >= end_time: break await asyncio.sleep(1) loop = asyncio.get_event_loop() # Blocking call which returns when the display_date() # coroutine is done loop.run_until_complete(display_date(loop)) loop.close()
  4. “It should be easy for (Python 3.3 ports of) frameworks

    like Twisted, Tornado, or even gevent to either adapt the default event loop implementation to their needs using a lightweight adapter or proxy, or to replace the default event loop implementation with an adaptation of their own event loop implementation.” “Interoperability - asyncio”
 https://www.python.org/dev/peps/pep-3156/
  5. “For this interoperability to be effective, the preferred direction of

    adaptation in third party frameworks is to keep the default event loop and adapt it to the framework's API. Ideally all third party frameworks would give up their own event loop implementation in favor of the standard implementation.” “Interoperability - asyncio”
 https://www.python.org/dev/peps/pep-3156/
  6. “Twisted is an async I/O thing, asyncio is an async

    I/O thing. Therefore they are the same kind of thing. I only need one kind of thing in each category of thing. Therefore I only need one of them, and the “standard” one is probably the better one to depend on. So I guess nobody will need Twisted any more!” https://glyph.twistedmatrix.com/2014/05/the-report-of-our-death.html
  7. 0 75000 150000 225000 300000 Twisted asyncio 3,352 107,612 21,902

    176,927 Code (lines) Comments (lines) Lines of Code (Python & C) With Tests
  8. 0 35000 70000 105000 140000 Twisted asyncio 2,355 54,242 8,452

    74,250 Code (lines) Comments (lines) Lines of Code (Python & C) Without Tests
  9. Twisted did a lot of things because none of these

    things were otherwise available
  10. 0 35000 70000 105000 140000 Twisted Django 25,625 54,242 74,033

    74,250 Code (lines) Comments (lines) Lines of Code (Python & C) Without Tests
  11. 0 7500 15000 22500 30000 Twisted
 (asyncio parity) asyncio
 (+

    concurrent.futures) 2,712 8,434 9,143 13,722 Code (lines) Comments (lines) Lines of Code (Python & C) Without Tests
  12. Similar to yield from, delegates to subgenerator Asynchronous code executed

    in a synchronous style Yielding for other things while it waits for a new result
  13. def _inlineCallbacks(result, g, deferred): """ See L{inlineCallbacks}. """ # This

    function is complicated by the need to prevent unbounded recursion # arising from repeatedly yielding immediately ready deferreds. This while # loop and the waiting variable solve that by manually unfolding the # recursion. waiting = [True, # waiting for result? None] # result while 1: try: # Send the last result back as the result of the yield expression. isFailure = isinstance(result, failure.Failure) if isFailure: result = result.throwExceptionIntoGenerator(g) else: result = g.send(result) except StopIteration as e: # fell off the end, or "return" statement deferred.callback(getattr(e, "value", None)) return deferred except _DefGen_Return as e: # returnValue() was called; time to give a result to the original # Deferred. First though, let's try to identify the potentially # confusing situation which results when returnValue() is # accidentally invoked from a different function, one that wasn't # decorated with @inlineCallbacks. # The traceback starts in this frame (the one for # _inlineCallbacks); the next one down should be the application # code. appCodeTrace = exc_info()[2].tb_next if isFailure: # If we invoked this generator frame by throwing an exception # into it, then throwExceptionIntoGenerator will consume an # additional stack frame itself, so we need to skip that too. appCodeTrace = appCodeTrace.tb_next # Now that we've identified the frame being exited by the # exception, let's figure out if returnValue was called from it # directly. returnValue itself consumes a stack frame, so the # application code will have a tb_next, but it will *not* have a # second tb_next. if appCodeTrace.tb_next.tb_next: # If returnValue was invoked non-local to the frame which it is # exiting, identify the frame that ultimately invoked # returnValue so that we can warn the user, as this behavior is # confusing. ultimateTrace = appCodeTrace while ultimateTrace.tb_next.tb_next: ultimateTrace = ultimateTrace.tb_next filename = ultimateTrace.tb_frame.f_code.co_filename lineno = ultimateTrace.tb_lineno warnings.warn_explicit( "returnValue() in %r causing %r to exit: " "returnValue should only be invoked by functions decorated " "with inlineCallbacks" % ( ultimateTrace.tb_frame.f_code.co_name, appCodeTrace.tb_frame.f_code.co_name), DeprecationWarning, filename, lineno) deferred.callback(e.value) return deferred except: deferred.errback() return deferred if isinstance(result, Deferred): # a deferred was yielded, get the result. def gotResult(r): if waiting[0]: waiting[0] = False waiting[1] = r else: _inlineCallbacks(r, g, deferred) result.addBoth(gotResult) if waiting[0]: # Haven't called back yet, set flag so that we get reinvoked # and return from the loop waiting[0] = False return deferred result = waiting[1] # Reset waiting to initial values for next loop. gotResult uses # waiting, but this isn't a problem because gotResult is only # executed once, and if it hasn't been executed yet, the return # branch above would have been taken. waiting[0] = True waiting[1] = None def inlineCallbacks(f): """ inlineCallbacks helps you write L{Deferred}-using code that looks like a regular sequential function. For example:: @inlineCallbacks def thingummy(): thing = yield makeSomeRequestResultingInDeferred() print(thing) # the result! hoorj! When you call anything that results in a L{Deferred}, you can simply yield it; your generator will automatically be resumed when the Deferred's result is available. The generator will be sent the result of the L{Deferred} with the 'send' method on generators, or if the result was a failure, 'throw'. Things that are not L{Deferred}s may also be yielded, and your generator will be resumed with the same object sent back. This means C{yield} performs an operation roughly equivalent to L{maybeDeferred}. Your inlineCallbacks-enabled generator will return a L{Deferred} object, which will result in the return value of the generator (or will fail with a failure object if your generator raises an unhandled exception). Note that you can't use C{return result} to return a value; use C{returnValue(result)} instead. Falling off the end of the generator, or simply using C{return} will cause the L{Deferred} to have a result of C{None}. Be aware that L{returnValue} will not accept a L{Deferred} as a parameter. If you believe the thing you'd like to return could be a L{Deferred}, do this:: result = yield result returnValue(result) The L{Deferred} returned from your deferred generator may errback if your generator raised an exception:: @inlineCallbacks def thingummy(): thing = yield makeSomeRequestResultingInDeferred() if thing == 'I love Twisted': # will become the result of the Deferred returnValue('TWISTED IS GREAT!') else: # will trigger an errback raise Exception('DESTROY ALL LIFE') If you are using Python 3.3 or later, it is possible to use the C{return} statement instead of L{returnValue}:: @inlineCallbacks def loadData(url): response = yield makeRequest(url) return json.loads(response) """ @wraps(f) def unwindGenerator(*args, **kwargs): try: gen = f(*args, **kwargs) except _DefGen_Return: raise TypeError( "inlineCallbacks requires %r to produce a generator; instead" "caught returnValue being used in a non-generator" % (f,)) if not isinstance(gen, types.GeneratorType): raise TypeError( "inlineCallbacks requires %r to produce a generator; " "instead got %r" % (f, gen)) return _inlineCallbacks(None, gen, Deferred()) return unwindGenerator
  14. def inlineCallbacks(f): @wraps(f) def unwindGenerator(*args, **kwargs): gen = f(*args, **kwargs)

    return _inlineCallbacks(None, gen, Deferred()) return unwindGenerator
  15. def inlineCallbacks(f): @wraps(f) def unwindGenerator(*args, **kwargs): gen = f(*args, **kwargs)

    return _inlineCallbacks(None, gen, Deferred()) return unwindGenerator 1 The wrapped function is called. This returns a generator.
  16. def inlineCallbacks(f): @wraps(f) def unwindGenerator(*args, **kwargs): gen = f(*args, **kwargs)

    return _inlineCallbacks(None, gen, Deferred()) return unwindGenerator 2 _inlineCallbacks is called with an initial result of None, the generator, and a new Deferred.
  17. def _inlineCallbacks(result, g, deferred): waiting = [True, None] # waiting

    for result and the result while 1: try: if isinstance(result, failure.Failure): result = result.throwExceptionIntoGenerator(g) else: result = g.send(result) except StopIteration as e: deferred.callback(getattr(e, "value", None)) return deferred except _DefGen_Return as e: deferred.callback(e.value) return deferred except: deferred.errback() return deferred We send result (None) into the generator, which spins it off and gets to the first Deferred 3
  18. deferred.callback(e.value) return deferred except: deferred.errback() return deferred if isinstance(result, Deferred):

    def gotResult(r): if waiting[0]: waiting = [False, r] else: _inlineCallbacks(r, g, deferred) result.addBoth(gotResult) if waiting[0]: waiting[0] = False return deferred result = waiting[1] waiting = [True, None] We tell the returned Deferred to call gotResult when it fires. 4
  19. deferred.callback(e.value) return deferred except: deferred.errback() return deferred if isinstance(result, Deferred):

    def gotResult(r): if waiting[0]: waiting = [False, r] else: _inlineCallbacks(r, g, deferred) result.addBoth(gotResult) if waiting[0]: waiting[0] = False return deferred result = waiting[1] waiting = [True, None] If we’re waiting for a result (which because this is the first run we are), return the Deferred from the original inlineCallbacks call, and say that we’re not waiting for a result. 5
  20. deferred.callback(e.value) return deferred except: deferred.errback() return deferred if isinstance(result, Deferred):

    def gotResult(r): if waiting[0]: waiting = [False, r] else: _inlineCallbacks(r, g, deferred) result.addBoth(gotResult) if waiting[0]: waiting[0] = False return deferred result = waiting[1] waiting = [True, None] When the Deferred returned by the generator fires, it checks to see if it is waiting for a result. If it is not, it calls _inlineCallbacks again with the new result. 6
  21. def _inlineCallbacks(result, g, deferred): waiting = [True, None] # waiting

    for result and the result while 1: try: if isinstance(result, failure.Failure): result = result.throwExceptionIntoGenerator(g) else: result = g.send(result) except StopIteration as e: deferred.callback(getattr(e, "value", None)) return deferred except _DefGen_Return as e: deferred.callback(e.value) return deferred except: deferred.errback() return deferred Now result is a value we have, so it is sent in to the generator. 7
  22. def _inlineCallbacks(result, g, deferred): waiting = [True, None] # waiting

    for result and the result while 1: try: if isinstance(result, failure.Failure): result = result.throwExceptionIntoGenerator(g) else: result = g.send(result) except StopIteration as e: deferred.callback(getattr(e, "value", None)) return deferred except _DefGen_Return as e: deferred.callback(e.value) return deferred except: deferred.errback() return deferred If this is the final result, we will get either StopIteration, _DefGen_Return, or an exception. 8
  23. def _inlineCallbacks(result, g, deferred): waiting = [True, None] # waiting

    for result and the result while 1: try: if isinstance(result, failure.Failure): result = result.throwExceptionIntoGenerator(g) else: result = g.send(result) except StopIteration as e: deferred.callback(getattr(e, "value", None)) return deferred except _DefGen_Return as e: deferred.callback(e.value) return deferred except: deferred.errback() return deferred StopIteration is when it runs off the “end”, or in Python 3, when you use return in a generator. 9a
  24. def _inlineCallbacks(result, g, deferred): waiting = [True, None] # waiting

    for result and the result while 1: try: if isinstance(result, failure.Failure): result = result.throwExceptionIntoGenerator(g) else: result = g.send(result) except StopIteration as e: deferred.callback(getattr(e, "value", None)) return deferred except _DefGen_Return as e: deferred.callback(e.value) return deferred except: deferred.errback() return deferred On Python 2, you can’t return from a generator. Twisted uses returnValue, which raises this special exception which holds a value. 9b
  25. def _inlineCallbacks(result, g, deferred): waiting = [True, None] # waiting

    for result and the result while 1: try: if isinstance(result, failure.Failure): result = result.throwExceptionIntoGenerator(g) else: result = g.send(result) except StopIteration as e: deferred.callback(getattr(e, "value", None)) return deferred except _DefGen_Return as e: deferred.callback(e.value) return deferred except: deferred.errback() return deferred Sometimes, the generator will raise an exception before it yields its next value. 9c
  26. def _inlineCallbacks(result, g, deferred): waiting = [True, None] # waiting

    for result and the result while 1: try: if isinstance(result, failure.Failure): result = result.throwExceptionIntoGenerator(g) else: result = g.send(result) except StopIteration as e: deferred.callback(getattr(e, "value", None)) return deferred except _DefGen_Return as e: deferred.callback(e.value) return deferred except: deferred.errback() return deferred In all three cases, the Deferred created in the initial inlineCallbacks call is fired with a result (or a failure). 9c 9b 9a
  27. class Future: def set_result(self, result): """Mark the future done and

    set its result. If the future is already done when this method is called, raises InvalidStateError. """ if self._state != _PENDING: raise InvalidStateError('{}: {!r}'.format(self._state, self)) self._result = result self._state = _FINISHED self._schedule_callbacks() def _schedule_callbacks(self): """Internal: Ask the event loop to call all callbacks. The callbacks are scheduled to be called as soon as possible. Also clears the callback list. """ callbacks = self._callbacks[:] if not callbacks: return self._callbacks[:] = [] for callback in callbacks: self._loop.call_soon(callback, self)
  28. deferred.callback(e.value) return deferred except: deferred.errback() return deferred if isinstance(result, Deferred):

    def gotResult(r): if waiting[0]: waiting = [False, r] else: _inlineCallbacks(r, g, deferred) result.addBoth(gotResult) if waiting[0]: waiting[0] = False return deferred result = waiting[1] waiting = [True, None] If the result Deferred already has a result, addBoth will be called synchronously 10
  29. deferred.callback(e.value) return deferred except: deferred.errback() return deferred if isinstance(result, Deferred):

    def gotResult(r): if waiting[0]: waiting = [False, r] else: _inlineCallbacks(r, g, deferred) result.addBoth(gotResult) if waiting[0]: waiting[0] = False return deferred result = waiting[1] waiting = [True, None] We ARE waiting for a result, and we have it, so we set it to say that we don’t, and put the result beside it. 11
  30. deferred.callback(e.value) return deferred except: deferred.errback() return deferred if isinstance(result, Deferred):

    def gotResult(r): if waiting[0]: waiting = [False, r] else: _inlineCallbacks(r, g, deferred) result.addBoth(gotResult) if waiting[0]: waiting[0] = False return deferred result = waiting[1] waiting = [True, None] Because we’re not waiting for a result, this code is ran, and we say that we want a new value. 12
  31. def _inlineCallbacks(result, g, deferred): waiting = [True, None] # waiting

    for result and the result while 1: try: if isinstance(result, failure.Failure): result = result.throwExceptionIntoGenerator(g) else: result = g.send(result) except StopIteration as e: deferred.callback(getattr(e, "value", None)) return deferred except _DefGen_Return as e: deferred.callback(e.value) return deferred except: deferred.errback() return deferred We loop back up, and send the synchronously-got result back into the generator. And so on, so forth. 13
  32. class Deferred: def __await__(self): awaiter = _MakeDeferredAwaitable(self) return awaiter.__await__() class

    _MakeDeferredAwaitable(object): """ A subgenerator which is created by L{Deferred.__await__}. """ result = _NO_RESULT def __init__(self, d): self.d = d self.d.addBoth(self._setResult) def _setResult(self, value): self.result = value def __next__(self): if self.result is not _NO_RESULT: raise StopIteration(self.result) return self.d def __await__(self): return self def _awaitTick(result, coro, deferred): try: result = coro.send(result) except StopIteration as e: deferred.callback(e.value) except: deferred.errback() if isinstance(result, Deferred): result.addBoth(_awaitTick, coro=coro, deferred=deferred) return deferred def deferredCoroutine(f): """ A decorator for supporting coroutine-style programming using L{Deferred}s. It implements the awaitable protocol from PEP-0492. When using this decorator, the wrapped function may use the C{await} keyword to suspend execution until the awaited L{Deferred} has fired. For example:: import treq from twisted.internet.defer import deferredCoroutine from twisted.internet.task import react def main(reactor): pages = [ "https://google.com/", "https://twistedmatrix.com", ] d = crawl(pages) d.addCallback(print) return d @deferredCoroutine async def crawl(pages): results = {} for page in pages: results[page] = await treq.content(await treq.get(page)) return results react(main) In the above example, L{treq.get} and L{treq.content} return L{Deferreds}, and the decorated function returns a L{Deferred} itself. """ def wrapped(*args, **kwargs): coro = f(*args, **kwargs) return _awaitTick(None, coro, Deferred()) return wrapped
  33. import treq from twisted.internet.defer import deferredCoroutine from twisted.internet.task import react

    def main(reactor): pages = [ "https://google.com/", "https://twistedmatrix.com", ] d = crawl(pages) d.addCallback(print) return d @deferredCoroutine async def crawl(pages): results = {} for page in pages: results[page] = await treq.content(await treq.get(page)) return results react(main)
  34. from twisted.internet import reactor from twisted.internet.endpoints import serverFromString from twisted.internet.protocol

    import Factory from twisted.protocols.basic import LineReceiver class MyProtocol(LineReceiver): def lineReceived(self, line): print("I got this line!") print(line) factory = Factory.forProtocol(MyProtocol) endpoint = serverFromString(reactor, "tcp:7000") endpoint.listen(factory) reactor.run()
  35. Example: hendrix WSGI runner on top of Twisted Websockets, TLS,

    run Twisted code github.com/hendrix/hendrix
  36. Example: Autobahn|Python Websockets for Twisted and asyncio WS + WAMP,

    a RPC & PubSub framework Super fast under PyPy! I work on it :D github.com/crossbario/autobahnpython
  37. Python 2.7 (all platforms) Python 3.4 (Linux, FreeBSD) Python 3.5

    (Linux, FreeBSD) Earlier versions support 2.6 and 3.3
  38. PyPy is close, only a handful of tests remain, nearly

    all are due to CPython assumptions