stack_context.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. #!/usr/bin/env python
  2. #
  3. # Copyright 2010 Facebook
  4. #
  5. # Licensed under the Apache License, Version 2.0 (the "License"); you may
  6. # not use this file except in compliance with the License. You may obtain
  7. # a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  13. # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  14. # License for the specific language governing permissions and limitations
  15. # under the License.
  16. """`StackContext` allows applications to maintain threadlocal-like state
  17. that follows execution as it moves to other execution contexts.
  18. The motivating examples are to eliminate the need for explicit
  19. ``async_callback`` wrappers (as in `tornado.web.RequestHandler`), and to
  20. allow some additional context to be kept for logging.
  21. This is slightly magic, but it's an extension of the idea that an
  22. exception handler is a kind of stack-local state and when that stack
  23. is suspended and resumed in a new context that state needs to be
  24. preserved. `StackContext` shifts the burden of restoring that state
  25. from each call site (e.g. wrapping each `.AsyncHTTPClient` callback
  26. in ``async_callback``) to the mechanisms that transfer control from
  27. one context to another (e.g. `.AsyncHTTPClient` itself, `.IOLoop`,
  28. thread pools, etc).
  29. Example usage::
  30. @contextlib.contextmanager
  31. def die_on_error():
  32. try:
  33. yield
  34. except Exception:
  35. logging.error("exception in asynchronous operation",exc_info=True)
  36. sys.exit(1)
  37. with StackContext(die_on_error):
  38. # Any exception thrown here *or in callback and its descendants*
  39. # will cause the process to exit instead of spinning endlessly
  40. # in the ioloop.
  41. http_client.fetch(url, callback)
  42. ioloop.start()
  43. Most applications shouldn't have to work with `StackContext` directly.
  44. Here are a few rules of thumb for when it's necessary:
  45. * If you're writing an asynchronous library that doesn't rely on a
  46. stack_context-aware library like `tornado.ioloop` or `tornado.iostream`
  47. (for example, if you're writing a thread pool), use
  48. `.stack_context.wrap()` before any asynchronous operations to capture the
  49. stack context from where the operation was started.
  50. * If you're writing an asynchronous library that has some shared
  51. resources (such as a connection pool), create those shared resources
  52. within a ``with stack_context.NullContext():`` block. This will prevent
  53. ``StackContexts`` from leaking from one request to another.
  54. * If you want to write something like an exception handler that will
  55. persist across asynchronous calls, create a new `StackContext` (or
  56. `ExceptionStackContext`), and make your asynchronous calls in a ``with``
  57. block that references your `StackContext`.
  58. """
  59. from __future__ import absolute_import, division, print_function, with_statement
  60. import sys
  61. import threading
  62. from .util import raise_exc_info
  63. class StackContextInconsistentError(Exception):
  64. pass
  65. class _State(threading.local):
  66. def __init__(self):
  67. self.contexts = (tuple(), None)
  68. _state = _State()
  69. class StackContext(object):
  70. """Establishes the given context as a StackContext that will be transferred.
  71. Note that the parameter is a callable that returns a context
  72. manager, not the context itself. That is, where for a
  73. non-transferable context manager you would say::
  74. with my_context():
  75. StackContext takes the function itself rather than its result::
  76. with StackContext(my_context):
  77. The result of ``with StackContext() as cb:`` is a deactivation
  78. callback. Run this callback when the StackContext is no longer
  79. needed to ensure that it is not propagated any further (note that
  80. deactivating a context does not affect any instances of that
  81. context that are currently pending). This is an advanced feature
  82. and not necessary in most applications.
  83. """
  84. def __init__(self, context_factory):
  85. self.context_factory = context_factory
  86. self.contexts = []
  87. self.active = True
  88. def _deactivate(self):
  89. self.active = False
  90. # StackContext protocol
  91. def enter(self):
  92. context = self.context_factory()
  93. self.contexts.append(context)
  94. context.__enter__()
  95. def exit(self, type, value, traceback):
  96. context = self.contexts.pop()
  97. context.__exit__(type, value, traceback)
  98. # Note that some of this code is duplicated in ExceptionStackContext
  99. # below. ExceptionStackContext is more common and doesn't need
  100. # the full generality of this class.
  101. def __enter__(self):
  102. self.old_contexts = _state.contexts
  103. self.new_contexts = (self.old_contexts[0] + (self,), self)
  104. _state.contexts = self.new_contexts
  105. try:
  106. self.enter()
  107. except:
  108. _state.contexts = self.old_contexts
  109. raise
  110. return self._deactivate
  111. def __exit__(self, type, value, traceback):
  112. try:
  113. self.exit(type, value, traceback)
  114. finally:
  115. final_contexts = _state.contexts
  116. _state.contexts = self.old_contexts
  117. # Generator coroutines and with-statements with non-local
  118. # effects interact badly. Check here for signs of
  119. # the stack getting out of sync.
  120. # Note that this check comes after restoring _state.context
  121. # so that if it fails things are left in a (relatively)
  122. # consistent state.
  123. if final_contexts is not self.new_contexts:
  124. raise StackContextInconsistentError(
  125. 'stack_context inconsistency (may be caused by yield '
  126. 'within a "with StackContext" block)')
  127. # Break up a reference to itself to allow for faster GC on CPython.
  128. self.new_contexts = None
  129. class ExceptionStackContext(object):
  130. """Specialization of StackContext for exception handling.
  131. The supplied ``exception_handler`` function will be called in the
  132. event of an uncaught exception in this context. The semantics are
  133. similar to a try/finally clause, and intended use cases are to log
  134. an error, close a socket, or similar cleanup actions. The
  135. ``exc_info`` triple ``(type, value, traceback)`` will be passed to the
  136. exception_handler function.
  137. If the exception handler returns true, the exception will be
  138. consumed and will not be propagated to other exception handlers.
  139. """
  140. def __init__(self, exception_handler):
  141. self.exception_handler = exception_handler
  142. self.active = True
  143. def _deactivate(self):
  144. self.active = False
  145. def exit(self, type, value, traceback):
  146. if type is not None:
  147. return self.exception_handler(type, value, traceback)
  148. def __enter__(self):
  149. self.old_contexts = _state.contexts
  150. self.new_contexts = (self.old_contexts[0], self)
  151. _state.contexts = self.new_contexts
  152. return self._deactivate
  153. def __exit__(self, type, value, traceback):
  154. try:
  155. if type is not None:
  156. return self.exception_handler(type, value, traceback)
  157. finally:
  158. final_contexts = _state.contexts
  159. _state.contexts = self.old_contexts
  160. if final_contexts is not self.new_contexts:
  161. raise StackContextInconsistentError(
  162. 'stack_context inconsistency (may be caused by yield '
  163. 'within a "with StackContext" block)')
  164. # Break up a reference to itself to allow for faster GC on CPython.
  165. self.new_contexts = None
  166. class NullContext(object):
  167. """Resets the `StackContext`.
  168. Useful when creating a shared resource on demand (e.g. an
  169. `.AsyncHTTPClient`) where the stack that caused the creating is
  170. not relevant to future operations.
  171. """
  172. def __enter__(self):
  173. self.old_contexts = _state.contexts
  174. _state.contexts = (tuple(), None)
  175. def __exit__(self, type, value, traceback):
  176. _state.contexts = self.old_contexts
  177. def _remove_deactivated(contexts):
  178. """Remove deactivated handlers from the chain"""
  179. # Clean ctx handlers
  180. stack_contexts = tuple([h for h in contexts[0] if h.active])
  181. # Find new head
  182. head = contexts[1]
  183. while head is not None and not head.active:
  184. head = head.old_contexts[1]
  185. # Process chain
  186. ctx = head
  187. while ctx is not None:
  188. parent = ctx.old_contexts[1]
  189. while parent is not None:
  190. if parent.active:
  191. break
  192. ctx.old_contexts = parent.old_contexts
  193. parent = parent.old_contexts[1]
  194. ctx = parent
  195. return (stack_contexts, head)
  196. def wrap(fn):
  197. """Returns a callable object that will restore the current `StackContext`
  198. when executed.
  199. Use this whenever saving a callback to be executed later in a
  200. different execution context (either in a different thread or
  201. asynchronously in the same thread).
  202. """
  203. # Check if function is already wrapped
  204. if fn is None or hasattr(fn, '_wrapped'):
  205. return fn
  206. # Capture current stack head
  207. # TODO: Any other better way to store contexts and update them in wrapped function?
  208. cap_contexts = [_state.contexts]
  209. if not cap_contexts[0][0] and not cap_contexts[0][1]:
  210. # Fast path when there are no active contexts.
  211. def null_wrapper(*args, **kwargs):
  212. try:
  213. current_state = _state.contexts
  214. _state.contexts = cap_contexts[0]
  215. return fn(*args, **kwargs)
  216. finally:
  217. _state.contexts = current_state
  218. null_wrapper._wrapped = True
  219. return null_wrapper
  220. def wrapped(*args, **kwargs):
  221. ret = None
  222. try:
  223. # Capture old state
  224. current_state = _state.contexts
  225. # Remove deactivated items
  226. cap_contexts[0] = contexts = _remove_deactivated(cap_contexts[0])
  227. # Force new state
  228. _state.contexts = contexts
  229. # Current exception
  230. exc = (None, None, None)
  231. top = None
  232. # Apply stack contexts
  233. last_ctx = 0
  234. stack = contexts[0]
  235. # Apply state
  236. for n in stack:
  237. try:
  238. n.enter()
  239. last_ctx += 1
  240. except:
  241. # Exception happened. Record exception info and store top-most handler
  242. exc = sys.exc_info()
  243. top = n.old_contexts[1]
  244. # Execute callback if no exception happened while restoring state
  245. if top is None:
  246. try:
  247. ret = fn(*args, **kwargs)
  248. except:
  249. exc = sys.exc_info()
  250. top = contexts[1]
  251. # If there was exception, try to handle it by going through the exception chain
  252. if top is not None:
  253. exc = _handle_exception(top, exc)
  254. else:
  255. # Otherwise take shorter path and run stack contexts in reverse order
  256. while last_ctx > 0:
  257. last_ctx -= 1
  258. c = stack[last_ctx]
  259. try:
  260. c.exit(*exc)
  261. except:
  262. exc = sys.exc_info()
  263. top = c.old_contexts[1]
  264. break
  265. else:
  266. top = None
  267. # If if exception happened while unrolling, take longer exception handler path
  268. if top is not None:
  269. exc = _handle_exception(top, exc)
  270. # If exception was not handled, raise it
  271. if exc != (None, None, None):
  272. raise_exc_info(exc)
  273. finally:
  274. _state.contexts = current_state
  275. return ret
  276. wrapped._wrapped = True
  277. return wrapped
  278. def _handle_exception(tail, exc):
  279. while tail is not None:
  280. try:
  281. if tail.exit(*exc):
  282. exc = (None, None, None)
  283. except:
  284. exc = sys.exc_info()
  285. tail = tail.old_contexts[1]
  286. return exc
  287. def run_with_stack_context(context, func):
  288. """Run a coroutine ``func`` in the given `StackContext`.
  289. It is not safe to have a ``yield`` statement within a ``with StackContext``
  290. block, so it is difficult to use stack context with `.gen.coroutine`.
  291. This helper function runs the function in the correct context while
  292. keeping the ``yield`` and ``with`` statements syntactically separate.
  293. Example::
  294. @gen.coroutine
  295. def incorrect():
  296. with StackContext(ctx):
  297. # ERROR: this will raise StackContextInconsistentError
  298. yield other_coroutine()
  299. @gen.coroutine
  300. def correct():
  301. yield run_with_stack_context(StackContext(ctx), other_coroutine)
  302. .. versionadded:: 3.1
  303. """
  304. with context:
  305. return func()