Home | History | Annotate | Download | only in paste
      1 # (c) 2005 Ben Bangert
      2 # This module is part of the Python Paste Project and is released under
      3 # the MIT License: http://www.opensource.org/licenses/mit-license.php
      4 """Registry for handling request-local module globals sanely
      5 
      6 Dealing with module globals in a thread-safe way is good if your
      7 application is the sole responder in a thread, however that approach fails
      8 to properly account for various scenarios that occur with WSGI applications
      9 and middleware.
     10 
     11 What is actually needed in the case where a module global is desired that
     12 is always set properly depending on the current request, is a stacked
     13 thread-local object. Such an object is popped or pushed during the request
     14 cycle so that it properly represents the object that should be active for
     15 the current request.
     16 
     17 To make it easy to deal with such variables, this module provides a special
     18 StackedObjectProxy class which you can instantiate and attach to your
     19 module where you'd like others to access it. The object you'd like this to
     20 actually "be" during the request is then registered with the
     21 RegistryManager middleware, which ensures that for the scope of the current
     22 WSGI application everything will work properly.
     23 
     24 Example:
     25 
     26 .. code-block:: python
     27 
     28     #yourpackage/__init__.py
     29 
     30     from paste.registry import RegistryManager, StackedObjectProxy
     31     myglobal = StackedObjectProxy()
     32 
     33     #wsgi app stack
     34     app = RegistryManager(yourapp)
     35 
     36     #inside your wsgi app
     37     class yourapp(object):
     38         def __call__(self, environ, start_response):
     39             obj = someobject  # The request-local object you want to access
     40                               # via yourpackage.myglobal
     41             if environ.has_key('paste.registry'):
     42                 environ['paste.registry'].register(myglobal, obj)
     43 
     44 You will then be able to import yourpackage anywhere in your WSGI app or in
     45 the calling stack below it and be assured that it is using the object you
     46 registered with Registry.
     47 
     48 RegistryManager can be in the WSGI stack multiple times, each time it
     49 appears it registers a new request context.
     50 
     51 
     52 Performance
     53 ===========
     54 
     55 The overhead of the proxy object is very minimal, however if you are using
     56 proxy objects extensively (Thousands of accesses per request or more), there
     57 are some ways to avoid them. A proxy object runs approximately 3-20x slower
     58 than direct access to the object, this is rarely your performance bottleneck
     59 when developing web applications.
     60 
     61 Should you be developing a system which may be accessing the proxy object
     62 thousands of times per request, the performance of the proxy will start to
     63 become more noticeable. In that circumstance, the problem can be avoided by
     64 getting at the actual object via the proxy with the ``_current_obj`` function:
     65 
     66 .. code-block:: python
     67 
     68     #sessions.py
     69     Session = StackedObjectProxy()
     70     # ... initialization code, etc.
     71 
     72     # somemodule.py
     73     import sessions
     74 
     75     def somefunc():
     76         session = sessions.Session._current_obj()
     77         # ... tons of session access
     78 
     79 This way the proxy is used only once to retrieve the object for the current
     80 context and the overhead is minimized while still making it easy to access
     81 the underlying object. The ``_current_obj`` function is preceded by an
     82 underscore to more likely avoid clashing with the contained object's
     83 attributes.
     84 
     85 **NOTE:** This is *highly* unlikely to be an issue in the vast majority of
     86 cases, and requires incredibly large amounts of proxy object access before
     87 one should consider the proxy object to be causing slow-downs. This section
     88 is provided solely in the extremely rare case that it is an issue so that a
     89 quick way to work around it is documented.
     90 
     91 """
     92 import six
     93 import paste.util.threadinglocal as threadinglocal
     94 
     95 __all__ = ['StackedObjectProxy', 'RegistryManager', 'StackedObjectRestorer',
     96            'restorer']
     97 
     98 class NoDefault(object): pass
     99 
    100 class StackedObjectProxy(object):
    101     """Track an object instance internally using a stack
    102 
    103     The StackedObjectProxy proxies access to an object internally using a
    104     stacked thread-local. This makes it safe for complex WSGI environments
    105     where access to the object may be desired in multiple places without
    106     having to pass the actual object around.
    107 
    108     New objects are added to the top of the stack with _push_object while
    109     objects can be removed with _pop_object.
    110 
    111     """
    112     def __init__(self, default=NoDefault, name="Default"):
    113         """Create a new StackedObjectProxy
    114 
    115         If a default is given, its used in every thread if no other object
    116         has been pushed on.
    117 
    118         """
    119         self.__dict__['____name__'] = name
    120         self.__dict__['____local__'] = threadinglocal.local()
    121         if default is not NoDefault:
    122             self.__dict__['____default_object__'] = default
    123 
    124     def __dir__(self):
    125         """Return a list of the StackedObjectProxy's and proxied
    126         object's (if one exists) names.
    127         """
    128         dir_list = dir(self.__class__) + self.__dict__.keys()
    129         try:
    130             dir_list.extend(dir(self._current_obj()))
    131         except TypeError:
    132             pass
    133         dir_list.sort()
    134         return dir_list
    135 
    136     def __getattr__(self, attr):
    137         return getattr(self._current_obj(), attr)
    138 
    139     def __setattr__(self, attr, value):
    140         setattr(self._current_obj(), attr, value)
    141 
    142     def __delattr__(self, name):
    143         delattr(self._current_obj(), name)
    144 
    145     def __getitem__(self, key):
    146         return self._current_obj()[key]
    147 
    148     def __setitem__(self, key, value):
    149         self._current_obj()[key] = value
    150 
    151     def __delitem__(self, key):
    152         del self._current_obj()[key]
    153 
    154     def __call__(self, *args, **kw):
    155         return self._current_obj()(*args, **kw)
    156 
    157     def __repr__(self):
    158         try:
    159             return repr(self._current_obj())
    160         except (TypeError, AttributeError):
    161             return '<%s.%s object at 0x%x>' % (self.__class__.__module__,
    162                                                self.__class__.__name__,
    163                                                id(self))
    164 
    165     def __iter__(self):
    166         return iter(self._current_obj())
    167 
    168     def __len__(self):
    169         return len(self._current_obj())
    170 
    171     def __contains__(self, key):
    172         return key in self._current_obj()
    173 
    174     def __nonzero__(self):
    175         return bool(self._current_obj())
    176 
    177     def _current_obj(self):
    178         """Returns the current active object being proxied to
    179 
    180         In the event that no object was pushed, the default object if
    181         provided will be used. Otherwise, a TypeError will be raised.
    182 
    183         """
    184         try:
    185             objects = self.____local__.objects
    186         except AttributeError:
    187             objects = None
    188         if objects:
    189             return objects[-1]
    190         else:
    191             obj = self.__dict__.get('____default_object__', NoDefault)
    192             if obj is not NoDefault:
    193                 return obj
    194             else:
    195                 raise TypeError(
    196                     'No object (name: %s) has been registered for this '
    197                     'thread' % self.____name__)
    198 
    199     def _push_object(self, obj):
    200         """Make ``obj`` the active object for this thread-local.
    201 
    202         This should be used like:
    203 
    204         .. code-block:: python
    205 
    206             obj = yourobject()
    207             module.glob = StackedObjectProxy()
    208             module.glob._push_object(obj)
    209             try:
    210                 ... do stuff ...
    211             finally:
    212                 module.glob._pop_object(conf)
    213 
    214         """
    215         try:
    216             self.____local__.objects.append(obj)
    217         except AttributeError:
    218             self.____local__.objects = []
    219             self.____local__.objects.append(obj)
    220 
    221     def _pop_object(self, obj=None):
    222         """Remove a thread-local object.
    223 
    224         If ``obj`` is given, it is checked against the popped object and an
    225         error is emitted if they don't match.
    226 
    227         """
    228         try:
    229             popped = self.____local__.objects.pop()
    230             if obj and popped is not obj:
    231                 raise AssertionError(
    232                     'The object popped (%s) is not the same as the object '
    233                     'expected (%s)' % (popped, obj))
    234         except AttributeError:
    235             raise AssertionError('No object has been registered for this thread')
    236 
    237     def _object_stack(self):
    238         """Returns all of the objects stacked in this container
    239 
    240         (Might return [] if there are none)
    241         """
    242         try:
    243             try:
    244                 objs = self.____local__.objects
    245             except AttributeError:
    246                 return []
    247             return objs[:]
    248         except AssertionError:
    249             return []
    250 
    251     # The following methods will be swapped for their original versions by
    252     # StackedObjectRestorer when restoration is enabled. The original
    253     # functions (e.g. _current_obj) will be available at _current_obj_orig
    254 
    255     def _current_obj_restoration(self):
    256         request_id = restorer.in_restoration()
    257         if request_id:
    258             return restorer.get_saved_proxied_obj(self, request_id)
    259         return self._current_obj_orig()
    260     _current_obj_restoration.__doc__ = \
    261         ('%s\n(StackedObjectRestorer restoration enabled)' % \
    262          _current_obj.__doc__)
    263 
    264     def _push_object_restoration(self, obj):
    265         if not restorer.in_restoration():
    266             self._push_object_orig(obj)
    267     _push_object_restoration.__doc__ = \
    268         ('%s\n(StackedObjectRestorer restoration enabled)' % \
    269          _push_object.__doc__)
    270 
    271     def _pop_object_restoration(self, obj=None):
    272         if not restorer.in_restoration():
    273             self._pop_object_orig(obj)
    274     _pop_object_restoration.__doc__ = \
    275         ('%s\n(StackedObjectRestorer restoration enabled)' % \
    276          _pop_object.__doc__)
    277 
    278 class Registry(object):
    279     """Track objects and stacked object proxies for removal
    280 
    281     The Registry object is instantiated a single time for the request no
    282     matter how many times the RegistryManager is used in a WSGI stack. Each
    283     RegistryManager must call ``prepare`` before continuing the call to
    284     start a new context for object registering.
    285 
    286     Each context is tracked with a dict inside a list. The last list
    287     element is the currently executing context. Each context dict is keyed
    288     by the id of the StackedObjectProxy instance being proxied, the value
    289     is a tuple of the StackedObjectProxy instance and the object being
    290     tracked.
    291 
    292     """
    293     def __init__(self):
    294         """Create a new Registry object
    295 
    296         ``prepare`` must still be called before this Registry object can be
    297         used to register objects.
    298 
    299         """
    300         self.reglist = []
    301 
    302     def prepare(self):
    303         """Used to create a new registry context
    304 
    305         Anytime a new RegistryManager is called, ``prepare`` needs to be
    306         called on the existing Registry object. This sets up a new context
    307         for registering objects.
    308 
    309         """
    310         self.reglist.append({})
    311 
    312     def register(self, stacked, obj):
    313         """Register an object with a StackedObjectProxy"""
    314         myreglist = self.reglist[-1]
    315         stacked_id = id(stacked)
    316         if stacked_id in myreglist:
    317             stacked._pop_object(myreglist[stacked_id][1])
    318             del myreglist[stacked_id]
    319         stacked._push_object(obj)
    320         myreglist[stacked_id] = (stacked, obj)
    321 
    322     def multiregister(self, stacklist):
    323         """Register a list of tuples
    324 
    325         Similar call semantics as register, except this registers
    326         multiple objects at once.
    327 
    328         Example::
    329 
    330             registry.multiregister([(sop, obj), (anothersop, anotherobj)])
    331 
    332         """
    333         myreglist = self.reglist[-1]
    334         for stacked, obj in stacklist:
    335             stacked_id = id(stacked)
    336             if stacked_id in myreglist:
    337                 stacked._pop_object(myreglist[stacked_id][1])
    338                 del myreglist[stacked_id]
    339             stacked._push_object(obj)
    340             myreglist[stacked_id] = (stacked, obj)
    341 
    342     # Replace now does the same thing as register
    343     replace = register
    344 
    345     def cleanup(self):
    346         """Remove all objects from all StackedObjectProxy instances that
    347         were tracked at this Registry context"""
    348         for stacked, obj in six.itervalues(self.reglist[-1]):
    349             stacked._pop_object(obj)
    350         self.reglist.pop()
    351 
    352 class RegistryManager(object):
    353     """Creates and maintains a Registry context
    354 
    355     RegistryManager creates a new registry context for the registration of
    356     StackedObjectProxy instances. Multiple RegistryManager's can be in a
    357     WSGI stack and will manage the context so that the StackedObjectProxies
    358     always proxy to the proper object.
    359 
    360     The object being registered can be any object sub-class, list, or dict.
    361 
    362     Registering objects is done inside a WSGI application under the
    363     RegistryManager instance, using the ``environ['paste.registry']``
    364     object which is a Registry instance.
    365 
    366     """
    367     def __init__(self, application, streaming=False):
    368         self.application = application
    369         self.streaming = streaming
    370 
    371     def __call__(self, environ, start_response):
    372         app_iter = None
    373         reg = environ.setdefault('paste.registry', Registry())
    374         reg.prepare()
    375         if self.streaming:
    376             return self.streaming_iter(reg, environ, start_response)
    377 
    378         try:
    379             app_iter = self.application(environ, start_response)
    380         except Exception as e:
    381             # Regardless of if the content is an iterable, generator, list
    382             # or tuple, we clean-up right now. If its an iterable/generator
    383             # care should be used to ensure the generator has its own ref
    384             # to the actual object
    385             if environ.get('paste.evalexception'):
    386                 # EvalException is present in the WSGI stack
    387                 expected = False
    388                 for expect in environ.get('paste.expected_exceptions', []):
    389                     if isinstance(e, expect):
    390                         expected = True
    391                 if not expected:
    392                     # An unexpected exception: save state for EvalException
    393                     restorer.save_registry_state(environ)
    394             reg.cleanup()
    395             raise
    396         except:
    397             # Save state for EvalException if it's present
    398             if environ.get('paste.evalexception'):
    399                 restorer.save_registry_state(environ)
    400             reg.cleanup()
    401             raise
    402         else:
    403             reg.cleanup()
    404 
    405         return app_iter
    406 
    407     def streaming_iter(self, reg, environ, start_response):
    408         try:
    409             for item in self.application(environ, start_response):
    410                 yield item
    411         except Exception as e:
    412             # Regardless of if the content is an iterable, generator, list
    413             # or tuple, we clean-up right now. If its an iterable/generator
    414             # care should be used to ensure the generator has its own ref
    415             # to the actual object
    416             if environ.get('paste.evalexception'):
    417                 # EvalException is present in the WSGI stack
    418                 expected = False
    419                 for expect in environ.get('paste.expected_exceptions', []):
    420                     if isinstance(e, expect):
    421                         expected = True
    422                 if not expected:
    423                     # An unexpected exception: save state for EvalException
    424                     restorer.save_registry_state(environ)
    425             reg.cleanup()
    426             raise
    427         except:
    428             # Save state for EvalException if it's present
    429             if environ.get('paste.evalexception'):
    430                 restorer.save_registry_state(environ)
    431             reg.cleanup()
    432             raise
    433         else:
    434             reg.cleanup()
    435 
    436 
    437 class StackedObjectRestorer(object):
    438     """Track StackedObjectProxies and their proxied objects for automatic
    439     restoration within EvalException's interactive debugger.
    440 
    441     An instance of this class tracks all StackedObjectProxy state in existence
    442     when unexpected exceptions are raised by WSGI applications housed by
    443     EvalException and RegistryManager. Like EvalException, this information is
    444     stored for the life of the process.
    445 
    446     When an unexpected exception occurs and EvalException is present in the
    447     WSGI stack, save_registry_state is intended to be called to store the
    448     Registry state and enable automatic restoration on all currently registered
    449     StackedObjectProxies.
    450 
    451     With restoration enabled, those StackedObjectProxies' _current_obj
    452     (overwritten by _current_obj_restoration) method's strategy is modified:
    453     it will return its appropriate proxied object from the restorer when
    454     a restoration context is active in the current thread.
    455 
    456     The StackedObjectProxies' _push/pop_object methods strategies are also
    457     changed: they no-op when a restoration context is active in the current
    458     thread (because the pushing/popping work is all handled by the
    459     Registry/restorer).
    460 
    461     The request's Registry objects' reglists are restored from the restorer
    462     when a restoration context begins, enabling the Registry methods to work
    463     while their changes are tracked by the restorer.
    464 
    465     The overhead of enabling restoration is negligible (another threadlocal
    466     access for the changed StackedObjectProxy methods) for normal use outside
    467     of a restoration context, but worth mentioning when combined with
    468     StackedObjectProxies normal overhead. Once enabled it does not turn off,
    469     however:
    470 
    471     o Enabling restoration only occurs after an unexpected exception is
    472     detected. The server is likely to be restarted shortly after the exception
    473     is raised to fix the cause
    474 
    475     o StackedObjectRestorer is only enabled when EvalException is enabled (not
    476     on a production server) and RegistryManager exists in the middleware
    477     stack"""
    478     def __init__(self):
    479         # Registries and their saved reglists by request_id
    480         self.saved_registry_states = {}
    481         self.restoration_context_id = threadinglocal.local()
    482 
    483     def save_registry_state(self, environ):
    484         """Save the state of this request's Registry (if it hasn't already been
    485         saved) to the saved_registry_states dict, keyed by the request's unique
    486         identifier"""
    487         registry = environ.get('paste.registry')
    488         if not registry or not len(registry.reglist) or \
    489                 self.get_request_id(environ) in self.saved_registry_states:
    490             # No Registry, no state to save, or this request's state has
    491             # already been saved
    492             return
    493 
    494         self.saved_registry_states[self.get_request_id(environ)] = \
    495             (registry, registry.reglist[:])
    496 
    497         # Tweak the StackedObjectProxies we want to save state for -- change
    498         # their methods to act differently when a restoration context is active
    499         # in the current thread
    500         for reglist in registry.reglist:
    501             for stacked, obj in six.itervalues(reglist):
    502                 self.enable_restoration(stacked)
    503 
    504     def get_saved_proxied_obj(self, stacked, request_id):
    505         """Retrieve the saved object proxied by the specified
    506         StackedObjectProxy for the request identified by request_id"""
    507         # All state for the request identified by request_id
    508         reglist = self.saved_registry_states[request_id][1]
    509 
    510         # The top of the stack was current when the exception occurred
    511         stack_level = len(reglist) - 1
    512         stacked_id = id(stacked)
    513         while True:
    514             if stack_level < 0:
    515                 # Nothing registered: Call _current_obj_orig to raise a
    516                 # TypeError
    517                 return stacked._current_obj_orig()
    518             context = reglist[stack_level]
    519             if stacked_id in context:
    520                 break
    521             # This StackedObjectProxy may not have been registered by the
    522             # RegistryManager that was active when the exception was raised --
    523             # continue searching down the stack until it's found
    524             stack_level -= 1
    525         return context[stacked_id][1]
    526 
    527     def enable_restoration(self, stacked):
    528         """Replace the specified StackedObjectProxy's methods with their
    529         respective restoration versions.
    530 
    531         _current_obj_restoration forces recovery of the saved proxied object
    532         when a restoration context is active in the current thread.
    533 
    534         _push/pop_object_restoration avoid pushing/popping data
    535         (pushing/popping is only done at the Registry level) when a restoration
    536         context is active in the current thread"""
    537         if '_current_obj_orig' in stacked.__dict__:
    538             # Restoration already enabled
    539             return
    540 
    541         for func_name in ('_current_obj', '_push_object', '_pop_object'):
    542             orig_func = getattr(stacked, func_name)
    543             restoration_func = getattr(stacked, func_name + '_restoration')
    544             stacked.__dict__[func_name + '_orig'] = orig_func
    545             stacked.__dict__[func_name] = restoration_func
    546 
    547     def get_request_id(self, environ):
    548         """Return a unique identifier for the current request"""
    549         from paste.evalexception.middleware import get_debug_count
    550         return get_debug_count(environ)
    551 
    552     def restoration_begin(self, request_id):
    553         """Enable a restoration context in the current thread for the specified
    554         request_id"""
    555         if request_id in self.saved_registry_states:
    556             # Restore the old Registry object's state
    557             registry, reglist = self.saved_registry_states[request_id]
    558             registry.reglist = reglist
    559 
    560         self.restoration_context_id.request_id = request_id
    561 
    562     def restoration_end(self):
    563         """Register a restoration context as finished, if one exists"""
    564         try:
    565             del self.restoration_context_id.request_id
    566         except AttributeError:
    567             pass
    568 
    569     def in_restoration(self):
    570         """Determine if a restoration context is active for the current thread.
    571         Returns the request_id it's active for if so, otherwise False"""
    572         return getattr(self.restoration_context_id, 'request_id', False)
    573 
    574 restorer = StackedObjectRestorer()
    575 
    576 
    577 # Paste Deploy entry point
    578 def make_registry_manager(app, global_conf):
    579     return RegistryManager(app)
    580 
    581 make_registry_manager.__doc__ = RegistryManager.__doc__
    582