stack_context.py
Go to the documentation of this file.
00001 #!/usr/bin/env python
00002 #
00003 # Copyright 2010 Facebook
00004 #
00005 # Licensed under the Apache License, Version 2.0 (the "License"); you may
00006 # not use this file except in compliance with the License. You may obtain
00007 # a copy of the License at
00008 #
00009 #     http://www.apache.org/licenses/LICENSE-2.0
00010 #
00011 # Unless required by applicable law or agreed to in writing, software
00012 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
00013 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
00014 # License for the specific language governing permissions and limitations
00015 # under the License.
00016 
00017 '''StackContext allows applications to maintain threadlocal-like state
00018 that follows execution as it moves to other execution contexts.
00019 
00020 The motivating examples are to eliminate the need for explicit
00021 async_callback wrappers (as in tornado.web.RequestHandler), and to
00022 allow some additional context to be kept for logging.
00023 
00024 This is slightly magic, but it's an extension of the idea that an exception
00025 handler is a kind of stack-local state and when that stack is suspended
00026 and resumed in a new context that state needs to be preserved.  StackContext
00027 shifts the burden of restoring that state from each call site (e.g.
00028 wrapping each AsyncHTTPClient callback in async_callback) to the mechanisms
00029 that transfer control from one context to another (e.g. AsyncHTTPClient
00030 itself, IOLoop, thread pools, etc).
00031 
00032 Example usage::
00033 
00034     @contextlib.contextmanager
00035     def die_on_error():
00036         try:
00037             yield
00038         except Exception:
00039             logging.error("exception in asynchronous operation",exc_info=True)
00040             sys.exit(1)
00041 
00042     with StackContext(die_on_error):
00043         # Any exception thrown here *or in callback and its desendents*
00044         # will cause the process to exit instead of spinning endlessly
00045         # in the ioloop.
00046         http_client.fetch(url, callback)
00047     ioloop.start()
00048 
00049 Most applications shouln't have to work with `StackContext` directly.
00050 Here are a few rules of thumb for when it's necessary:
00051 
00052 * If you're writing an asynchronous library that doesn't rely on a
00053   stack_context-aware library like `tornado.ioloop` or `tornado.iostream`
00054   (for example, if you're writing a thread pool), use
00055   `stack_context.wrap()` before any asynchronous operations to capture the
00056   stack context from where the operation was started.
00057 
00058 * If you're writing an asynchronous library that has some shared
00059   resources (such as a connection pool), create those shared resources
00060   within a ``with stack_context.NullContext():`` block.  This will prevent
00061   ``StackContexts`` from leaking from one request to another.
00062 
00063 * If you want to write something like an exception handler that will
00064   persist across asynchronous calls, create a new `StackContext` (or
00065   `ExceptionStackContext`), and make your asynchronous calls in a ``with``
00066   block that references your `StackContext`.
00067 '''
00068 
00069 from __future__ import absolute_import, division, with_statement
00070 
00071 import contextlib
00072 import functools
00073 import itertools
00074 import operator
00075 import sys
00076 import threading
00077 
00078 from tornado.util import raise_exc_info
00079 
00080 
00081 class _State(threading.local):
00082     def __init__(self):
00083         self.contexts = ()
00084 _state = _State()
00085 
00086 
00087 class StackContext(object):
00088     '''Establishes the given context as a StackContext that will be transferred.
00089 
00090     Note that the parameter is a callable that returns a context
00091     manager, not the context itself.  That is, where for a
00092     non-transferable context manager you would say::
00093 
00094       with my_context():
00095 
00096     StackContext takes the function itself rather than its result::
00097 
00098       with StackContext(my_context):
00099 
00100     The result of ``with StackContext() as cb:`` is a deactivation
00101     callback.  Run this callback when the StackContext is no longer
00102     needed to ensure that it is not propagated any further (note that
00103     deactivating a context does not affect any instances of that
00104     context that are currently pending).  This is an advanced feature
00105     and not necessary in most applications.
00106     '''
00107     def __init__(self, context_factory, _active_cell=None):
00108         self.context_factory = context_factory
00109         self.active_cell = _active_cell or [True]
00110 
00111     # Note that some of this code is duplicated in ExceptionStackContext
00112     # below.  ExceptionStackContext is more common and doesn't need
00113     # the full generality of this class.
00114     def __enter__(self):
00115         self.old_contexts = _state.contexts
00116         # _state.contexts is a tuple of (class, arg, active_cell) tuples
00117         _state.contexts = (self.old_contexts +
00118                            ((StackContext, self.context_factory, self.active_cell),))
00119         try:
00120             self.context = self.context_factory()
00121             self.context.__enter__()
00122         except Exception:
00123             _state.contexts = self.old_contexts
00124             raise
00125         return lambda: operator.setitem(self.active_cell, 0, False)
00126 
00127     def __exit__(self, type, value, traceback):
00128         try:
00129             return self.context.__exit__(type, value, traceback)
00130         finally:
00131             _state.contexts = self.old_contexts
00132 
00133 
00134 class ExceptionStackContext(object):
00135     '''Specialization of StackContext for exception handling.
00136 
00137     The supplied exception_handler function will be called in the
00138     event of an uncaught exception in this context.  The semantics are
00139     similar to a try/finally clause, and intended use cases are to log
00140     an error, close a socket, or similar cleanup actions.  The
00141     exc_info triple (type, value, traceback) will be passed to the
00142     exception_handler function.
00143 
00144     If the exception handler returns true, the exception will be
00145     consumed and will not be propagated to other exception handlers.
00146     '''
00147     def __init__(self, exception_handler, _active_cell=None):
00148         self.exception_handler = exception_handler
00149         self.active_cell = _active_cell or [True]
00150 
00151     def __enter__(self):
00152         self.old_contexts = _state.contexts
00153         _state.contexts = (self.old_contexts +
00154                            ((ExceptionStackContext, self.exception_handler,
00155                              self.active_cell),))
00156         return lambda: operator.setitem(self.active_cell, 0, False)
00157 
00158     def __exit__(self, type, value, traceback):
00159         try:
00160             if type is not None:
00161                 return self.exception_handler(type, value, traceback)
00162         finally:
00163             _state.contexts = self.old_contexts
00164 
00165 
00166 class NullContext(object):
00167     '''Resets the StackContext.
00168 
00169     Useful when creating a shared resource on demand (e.g. an AsyncHTTPClient)
00170     where the stack that caused the creating is not relevant to future
00171     operations.
00172     '''
00173     def __enter__(self):
00174         self.old_contexts = _state.contexts
00175         _state.contexts = ()
00176 
00177     def __exit__(self, type, value, traceback):
00178         _state.contexts = self.old_contexts
00179 
00180 
00181 class _StackContextWrapper(functools.partial):
00182     pass
00183 
00184 
00185 def wrap(fn):
00186     '''Returns a callable object that will restore the current StackContext
00187     when executed.
00188 
00189     Use this whenever saving a callback to be executed later in a
00190     different execution context (either in a different thread or
00191     asynchronously in the same thread).
00192     '''
00193     if fn is None or fn.__class__ is _StackContextWrapper:
00194         return fn
00195     # functools.wraps doesn't appear to work on functools.partial objects
00196     #@functools.wraps(fn)
00197 
00198     def wrapped(callback, contexts, *args, **kwargs):
00199         if contexts is _state.contexts or not contexts:
00200             callback(*args, **kwargs)
00201             return
00202         if not _state.contexts:
00203             new_contexts = [cls(arg, active_cell)
00204                             for (cls, arg, active_cell) in contexts
00205                             if active_cell[0]]
00206         # If we're moving down the stack, _state.contexts is a prefix
00207         # of contexts.  For each element of contexts not in that prefix,
00208         # create a new StackContext object.
00209         # If we're moving up the stack (or to an entirely different stack),
00210         # _state.contexts will have elements not in contexts.  Use
00211         # NullContext to clear the state and then recreate from contexts.
00212         elif (len(_state.contexts) > len(contexts) or
00213             any(a[1] is not b[1]
00214                 for a, b in itertools.izip(_state.contexts, contexts))):
00215             # contexts have been removed or changed, so start over
00216             new_contexts = ([NullContext()] +
00217                             [cls(arg, active_cell)
00218                              for (cls, arg, active_cell) in contexts
00219                              if active_cell[0]])
00220         else:
00221             new_contexts = [cls(arg, active_cell)
00222                             for (cls, arg, active_cell) in contexts[len(_state.contexts):]
00223                             if active_cell[0]]
00224         if len(new_contexts) > 1:
00225             with _nested(*new_contexts):
00226                 callback(*args, **kwargs)
00227         elif new_contexts:
00228             with new_contexts[0]:
00229                 callback(*args, **kwargs)
00230         else:
00231             callback(*args, **kwargs)
00232     if _state.contexts:
00233         return _StackContextWrapper(wrapped, fn, _state.contexts)
00234     else:
00235         return _StackContextWrapper(fn)
00236 
00237 
00238 @contextlib.contextmanager
00239 def _nested(*managers):
00240     """Support multiple context managers in a single with-statement.
00241 
00242     Copied from the python 2.6 standard library.  It's no longer present
00243     in python 3 because the with statement natively supports multiple
00244     context managers, but that doesn't help if the list of context
00245     managers is not known until runtime.
00246     """
00247     exits = []
00248     vars = []
00249     exc = (None, None, None)
00250     try:
00251         for mgr in managers:
00252             exit = mgr.__exit__
00253             enter = mgr.__enter__
00254             vars.append(enter())
00255             exits.append(exit)
00256         yield vars
00257     except:
00258         exc = sys.exc_info()
00259     finally:
00260         while exits:
00261             exit = exits.pop()
00262             try:
00263                 if exit(*exc):
00264                     exc = (None, None, None)
00265             except:
00266                 exc = sys.exc_info()
00267         if exc != (None, None, None):
00268             # Don't rely on sys.exc_info() still containing
00269             # the right information. Another exception may
00270             # have been raised and caught by an exit method
00271             raise_exc_info(exc)


roswww
Author(s): Jonathan Mace
autogenerated on Thu Jan 2 2014 11:53:30