3 # Copyright 2010 Facebook
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
9 # http://www.apache.org/licenses/LICENSE-2.0
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
17 """`StackContext` allows applications to maintain threadlocal-like state
18 that follows execution as it moves to other execution contexts.
20 The motivating examples are to eliminate the need for explicit
21 ``async_callback`` wrappers (as in `tornado.web.RequestHandler`), and to
22 allow some additional context to be kept for logging.
24 This is slightly magic, but it's an extension of the idea that an
25 exception handler is a kind of stack-local state and when that stack
26 is suspended and resumed in a new context that state needs to be
27 preserved. `StackContext` shifts the burden of restoring that state
28 from each call site (e.g. wrapping each `.AsyncHTTPClient` callback
29 in ``async_callback``) to the mechanisms that transfer control from
30 one context to another (e.g. `.AsyncHTTPClient` itself, `.IOLoop`,
35 @contextlib.contextmanager
40 logging.error("exception in asynchronous operation",exc_info=True)
43 with StackContext(die_on_error):
44 # Any exception thrown here *or in callback and its desendents*
45 # will cause the process to exit instead of spinning endlessly
47 http_client.fetch(url, callback)
50 Most applications shouln't have to work with `StackContext` directly.
51 Here are a few rules of thumb for when it's necessary:
53 * If you're writing an asynchronous library that doesn't rely on a
54 stack_context-aware library like `tornado.ioloop` or `tornado.iostream`
55 (for example, if you're writing a thread pool), use
56 `.stack_context.wrap()` before any asynchronous operations to capture the
57 stack context from where the operation was started.
59 * If you're writing an asynchronous library that has some shared
60 resources (such as a connection pool), create those shared resources
61 within a ``with stack_context.NullContext():`` block. This will prevent
62 ``StackContexts`` from leaking from one request to another.
64 * If you want to write something like an exception handler that will
65 persist across asynchronous calls, create a new `StackContext` (or
66 `ExceptionStackContext`), and make your asynchronous calls in a ``with``
67 block that references your `StackContext`.
70 from __future__ import absolute_import, division, print_function, with_statement
75 from .util import raise_exc_info
78 class StackContextInconsistentError(Exception):
82 class _State(threading.local):
84 self.contexts = (tuple(), None)
88 class StackContext(object):
89 """Establishes the given context as a StackContext that will be transferred.
91 Note that the parameter is a callable that returns a context
92 manager, not the context itself. That is, where for a
93 non-transferable context manager you would say::
97 StackContext takes the function itself rather than its result::
99 with StackContext(my_context):
101 The result of ``with StackContext() as cb:`` is a deactivation
102 callback. Run this callback when the StackContext is no longer
103 needed to ensure that it is not propagated any further (note that
104 deactivating a context does not affect any instances of that
105 context that are currently pending). This is an advanced feature
106 and not necessary in most applications.
108 def __init__(self, context_factory):
109 self.context_factory = context_factory
113 def _deactivate(self):
116 # StackContext protocol
118 context = self.context_factory()
119 self.contexts.append(context)
122 def exit(self, type, value, traceback):
123 context = self.contexts.pop()
124 context.__exit__(type, value, traceback)
126 # Note that some of this code is duplicated in ExceptionStackContext
127 # below. ExceptionStackContext is more common and doesn't need
128 # the full generality of this class.
130 self.old_contexts = _state.contexts
131 self.new_contexts = (self.old_contexts[0] + (self,), self)
132 _state.contexts = self.new_contexts
137 _state.contexts = self.old_contexts
140 return self._deactivate
142 def __exit__(self, type, value, traceback):
144 self.exit(type, value, traceback)
146 final_contexts = _state.contexts
147 _state.contexts = self.old_contexts
149 # Generator coroutines and with-statements with non-local
150 # effects interact badly. Check here for signs of
151 # the stack getting out of sync.
152 # Note that this check comes after restoring _state.context
153 # so that if it fails things are left in a (relatively)
155 if final_contexts is not self.new_contexts:
156 raise StackContextInconsistentError(
157 'stack_context inconsistency (may be caused by yield '
158 'within a "with StackContext" block)')
160 # Break up a reference to itself to allow for faster GC on CPython.
161 self.new_contexts = None
164 class ExceptionStackContext(object):
165 """Specialization of StackContext for exception handling.
167 The supplied ``exception_handler`` function will be called in the
168 event of an uncaught exception in this context. The semantics are
169 similar to a try/finally clause, and intended use cases are to log
170 an error, close a socket, or similar cleanup actions. The
171 ``exc_info`` triple ``(type, value, traceback)`` will be passed to the
172 exception_handler function.
174 If the exception handler returns true, the exception will be
175 consumed and will not be propagated to other exception handlers.
177 def __init__(self, exception_handler):
178 self.exception_handler = exception_handler
181 def _deactivate(self):
184 def exit(self, type, value, traceback):
186 return self.exception_handler(type, value, traceback)
189 self.old_contexts = _state.contexts
190 self.new_contexts = (self.old_contexts[0], self)
191 _state.contexts = self.new_contexts
193 return self._deactivate
195 def __exit__(self, type, value, traceback):
198 return self.exception_handler(type, value, traceback)
200 final_contexts = _state.contexts
201 _state.contexts = self.old_contexts
203 if final_contexts is not self.new_contexts:
204 raise StackContextInconsistentError(
205 'stack_context inconsistency (may be caused by yield '
206 'within a "with StackContext" block)')
208 # Break up a reference to itself to allow for faster GC on CPython.
209 self.new_contexts = None
212 class NullContext(object):
213 """Resets the `StackContext`.
215 Useful when creating a shared resource on demand (e.g. an
216 `.AsyncHTTPClient`) where the stack that caused the creating is
217 not relevant to future operations.
220 self.old_contexts = _state.contexts
221 _state.contexts = (tuple(), None)
223 def __exit__(self, type, value, traceback):
224 _state.contexts = self.old_contexts
227 def _remove_deactivated(contexts):
228 """Remove deactivated handlers from the chain"""
230 stack_contexts = tuple([h for h in contexts[0] if h.active])
234 while head is not None and not head.active:
235 head = head.old_contexts[1]
239 while ctx is not None:
240 parent = ctx.old_contexts[1]
242 while parent is not None:
245 ctx.old_contexts = parent.old_contexts
246 parent = parent.old_contexts[1]
250 return (stack_contexts, head)
254 """Returns a callable object that will restore the current `StackContext`
257 Use this whenever saving a callback to be executed later in a
258 different execution context (either in a different thread or
259 asynchronously in the same thread).
261 # Check if function is already wrapped
262 if fn is None or hasattr(fn, '_wrapped'):
265 # Capture current stack head
266 # TODO: Any other better way to store contexts and update them in wrapped function?
267 cap_contexts = [_state.contexts]
269 def wrapped(*args, **kwargs):
273 current_state = _state.contexts
275 # Remove deactivated items
276 cap_contexts[0] = contexts = _remove_deactivated(cap_contexts[0])
279 _state.contexts = contexts
282 exc = (None, None, None)
285 # Apply stack contexts
295 # Exception happened. Record exception info and store top-most handler
297 top = n.old_contexts[1]
299 # Execute callback if no exception happened while restoring state
302 ret = fn(*args, **kwargs)
307 # If there was exception, try to handle it by going through the exception chain
309 exc = _handle_exception(top, exc)
311 # Otherwise take shorter path and run stack contexts in reverse order
320 top = c.old_contexts[1]
325 # If if exception happened while unrolling, take longer exception handler path
327 exc = _handle_exception(top, exc)
329 # If exception was not handled, raise it
330 if exc != (None, None, None):
333 _state.contexts = current_state
336 wrapped._wrapped = True
340 def _handle_exception(tail, exc):
341 while tail is not None:
344 exc = (None, None, None)
348 tail = tail.old_contexts[1]
353 def run_with_stack_context(context, func):
354 """Run a coroutine ``func`` in the given `StackContext`.
356 It is not safe to have a ``yield`` statement within a ``with StackContext``
357 block, so it is difficult to use stack context with `.gen.coroutine`.
358 This helper function runs the function in the correct context while
359 keeping the ``yield`` and ``with`` statements syntactically separate.
365 with StackContext(ctx):
366 # ERROR: this will raise StackContextInconsistentError
367 yield other_coroutine()
371 yield run_with_stack_context(StackContext(ctx), other_coroutine)
373 .. versionadded:: 3.1