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

PyConZA 2014: "Monkeying around with Twisted" by Richard Spiers

Pycon ZA
October 03, 2014

PyConZA 2014: "Monkeying around with Twisted" by Richard Spiers

Twisted is an open source framework for writing network based services. It utilises an asynchronous, event driven model which allows the rapid development of custom network protocols. While Twisted makes implementing network services much easier, it comes with its own set of challenges. One of these challenges involves tracking the context of multiple requests. In a traditional web server the thread ID could be used to track a particular request and modify log entries appropriately. However, this does not work in Twisted as it utilises a single thread (generally speaking).

We have chosen to tackle this issue, amongst others, by monkey patching some of the Twisted subsystems.

Through this talk we will introduce Twisted as a viable option for programming network based services. No previous Twisted knowledge is required, and the concepts introduced will be explained through real world examples of restful controllers implemented in Twisted. This primer will be followed by a discussion on our solution to the context tracking problem as well as some of the other areas in which we have found monkey patching to be beneficial.

To summarise, this talk will:

* introduce the core concepts in Twisted
* discuss how we track related actions across multiple disparate services and nodes
* tag our log entries with identifying context
* handle time jumps on underlying infrastructure
* implement clean unit tests for different environments

The talk is aimed at a general Python audience so no previous experience with Twisted is required. However, we believe that our monkey patching approach will be of interest to people currently using Twisted as well.

Pycon ZA

October 03, 2014
Tweet

More Decks by Pycon ZA

Other Decks in Programming

Transcript

  1. 16

  2. 42 @classmethod def set_current_context(cls, context): cls.current[thread.get_ident()] = context Our context

    class Context(object): current = {} @classmethod def get_current_context(cls): return cls.current.get(thread.get_ident(), None)
  3. 45 Context class DeferredWithContext(Deferred): def __init__(self, *args, **kwargs): self._context =

    base.get_context() Deferred.__init__(self, *args, **kwargs) Deferred = defer.Deferred ...
  4. 46 Context def callback(self, result): old_context = base.get_context() base.set_context(self._context) try:

    Deferred.callback(self, result) finally: base.set_context(old_context)
  5. 47 Context def errback(self, fail=None): old_context = base.get_context() base.set_context(self._context) try:

    Deferred.errback(self, fail) finally: base.set_context(old_context) defer.Deferred = DeferredWithContext
  6. 48 Context def _func_with_context(context, f, args, kwargs): old_context = base.get_context()

    base.set_context(context) try: return f(*args, **kwargs) finally: base.set_context(old_context)
  7. 49 Context def callLaterWithContext(sec, f, *args, **kwargs): context = base.get_context()

    return callLater(sec, _func_with_context, context, f, args, kwargs) def add_context_to_reactor(reactor): callLater = reactor.callLater reactor.callLater = callLaterWithContext
  8. 50 Context Apply context on receipt of request Decoding HTTP

    request Consuming AMQP message Now can insert context into requests As part of HTTP request As part of AMQP msg
  9. 52 Logging Context class _Logger(logging.Logger): def _log(self, level, msg, *args,

    **kwargs): extra = kwargs.setdefault('extra', {}) if 'context' not in extra: extra['context'] = getattr(get_context(), 'id', None) return logging.Logger._log( self, level, msg, *args, **kwargs)
  10. 54 Back to the event loop ! for event in

    events: event.process() self.doIteration(t) Actually
  11. 55 Several different reactors are provided by Twisted: select poll

    epoll IOCP GTK+ 2.0 Tkinter wxPython Win32 CoreFoundation
  12. 56 General Idea: Set of readers or writers Sets containing

    FileDescriptor instances which will be checked for read events or writability self._reads = set() self._writes = set()
  13. 57 General Idea: methods to add or remove these readers

    / writers Add a FileDescriptor instances for notification of data available to write or read addReader addWriter
  14. 58 General Idea: methods doing a read or write for

    selectables, method, fdset in ((r, "doRead", self._reads), (w,”doWrite", self._writes)): doRead doWrite
  15. 63 Targets to patch def doIteration(timeout): before = reactor.seconds() blockingIOTimer.reset()

    timeout = min(timeout, MAX_DELAY) oldIteration(timeout) sleep_end = blockingIOTimer.timeOfFirstCall() now = reactor.seconds()
  16. 64 Targets to patch if not sleep_end: sleep_end = now

    if sleep_end < before: resetTimers(sleep_end, before) elif MAX_JUMP and (sleep_end-before > MAX_JUMP): resetTimers(sleep_end, before) elif now - before > MAX_BLOCKING: log.warn('Total IO processing time blocked reactor for %s seconds', now-before)
  17. 68 TimedReaderWriter def _doClassRead(): instance = ref() if instance is

    not None: return self._timeCall( class_.doRead.__get__(instance, class_)) if needsRead: def _doInstanceRead(): return self._timeCall(originalDoRead)
  18. 69 TimedReaderWriter _doRead = _doInstanceRead class_method = getattr(class_, 'doRead', None)

    class_func = getattr(class_method, 'im_func', None) instance_func = getattr(reader_or_writer.doRead, 'im_func', None) if class_func and instance_func and class_func == instance_func: _doRead = _doClassRead
  19. 72 Monitor for blocking reactor def _timeCall(self, func, *args, **kwargs):

    start = self.reactor.seconds() if not self.first_event: self.first_event = start try: result = func(*args, **kwargs) return result
  20. 73 Monitor for blocking reactor finally: end = self.reactor.seconds() if

    (end - start > BLOCKING): self.log.warn(‘ Potential blocking call discovered (took %s seconds) while executing %s’, end-start, func)
  21. 74 @contextmanager def timedCallContext(func, args, kwargs): before = reactor.seconds() try:

    yield finally: now = reactor.seconds() if now - before > BLOCKING: Monitor for blocking reactor
  22. 75 module = getattr(func, '__module__', '__main__') func_name = getattr(func, '__name__',

    repr(func)) log.warn('DelayedCall blocked reactor for %s seconds: %s.%s(%s%s%s)', now-before, module, func_name, ', '.join([repr(x) for x in args]), ', ' if (bool(kwargs) and bool(args)) else '', ', '.join(['%s=%s' % (k, repr(v)) for k, v in kwargs.items()])) Monitor for blocking reactor
  23. 76 Handle time jumps def resetTimers(now, before): delta = now

    - before if delta < 0: log.warn(“ Travelled back in time by %s seconds", -delta) else: log.warn(“ Travelled forward in time by %s seconds”, delta)
  24. 79 MonkeyPatch() def __init__(self, globalContext, localContext, patchDict): self._originalDict = {}

    self._patchDict = patchDict self._context = {} for k, v in globalContext.iteritems(): self._context[k] = v for k, v in localContext.iteritems(): self._context[k] = v
  25. 83 MonkeyPatch() Usage patch = {} patch[‘moduleA.submodule.VALUE’] = new_value patch[‘moduleB.submodule.VALUE’]

    = new_value ... with MonkeyPatch(globals(), locals(), patch): #Monkey patched scope .... # Monkey patching reverted