server/hook.py
changeset 2835 04034421b072
child 2840 06daf13195d4
equal deleted inserted replaced
2834:7df3494ae657 2835:04034421b072
       
     1 """Hooks management
       
     2 
       
     3 This module defined the `Hook` class and registry and a set of abstract classes
       
     4 for operations.
       
     5 
       
     6 
       
     7 Hooks are called before / after any individual update of entities / relations
       
     8 in the repository and on special events such as server startup or shutdown.
       
     9 
       
    10 
       
    11 Operations may be registered by hooks during a transaction, which will  be
       
    12 fired when the pool is commited or rollbacked.
       
    13 
       
    14 
       
    15 Entity hooks (eg before_add_entity, after_add_entity, before_update_entity,
       
    16 after_update_entity, before_delete_entity, after_delete_entity) all have an
       
    17 `entity` attribute
       
    18 
       
    19 Relation (eg before_add_relation, after_add_relation, before_delete_relation,
       
    20 after_delete_relation) all have `eidfrom`, `rtype`, `eidto` attributes.
       
    21 
       
    22 Server start/stop hooks (eg server_startup, server_shutdown) have a `repo`
       
    23 attribute, but *their `cw_req` attribute is None*.
       
    24 
       
    25 Backup/restore hooks (eg server_backup, server_restore) have a `repo` and a
       
    26 `timestamp` attributes, but *their `cw_req` attribute is None*.
       
    27 
       
    28 Session hooks (eg session_open, session_close) have no special attribute.
       
    29 
       
    30 
       
    31 :organization: Logilab
       
    32 :copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
       
    33 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
    34 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
       
    35 """
       
    36 __docformat__ = "restructuredtext en"
       
    37 
       
    38 from warnings import warn
       
    39 from logging import getLogger
       
    40 
       
    41 from logilab.common.decorators import classproperty
       
    42 from logilab.common.logging_ext import set_log_methods
       
    43 
       
    44 from cubicweb.cwvreg import CWRegistry, VRegistry
       
    45 from cubicweb.selectors import (objectify_selector, lltrace, match_search_state,
       
    46                                 entity_implements)
       
    47 from cubicweb.appobject import AppObject
       
    48 
       
    49 
       
    50 ENTITIES_HOOKS = set(('before_add_entity',    'after_add_entity',
       
    51                       'before_update_entity', 'after_update_entity',
       
    52                       'before_delete_entity', 'after_delete_entity'))
       
    53 RELATIONS_HOOKS = set(('before_add_relation',   'after_add_relation' ,
       
    54                        'before_delete_relation','after_delete_relation'))
       
    55 SYSTEM_HOOKS = set(('server_backup', 'server_restore',
       
    56                     'server_startup', 'server_shutdown',
       
    57                     'session_open', 'session_close'))
       
    58 ALL_HOOKS = ENTITIES_HOOKS | RELATIONS_HOOKS | SYSTEM_HOOKS
       
    59 
       
    60 
       
    61 class HooksRegistry(CWRegistry):
       
    62 
       
    63     def register(self, obj, **kwargs):
       
    64         for event in obj.events:
       
    65             if event not in ALL_HOOKS:
       
    66                 raise Exception('bad event %s on %s' % (event, obj))
       
    67         super(HooksRegistry, self).register(obj, **kwargs)
       
    68 
       
    69     def call_hooks(self, event, req=None, **kwargs):
       
    70         kwargs['event'] = event
       
    71         # XXX remove .enabled
       
    72         for hook in sorted([x for x in self.possible_objects(req, **kwargs)
       
    73                             if x.enabled], key=lambda x: x.order):
       
    74             hook()
       
    75 
       
    76 VRegistry.REGISTRY_FACTORY['hooks'] = HooksRegistry
       
    77 
       
    78 
       
    79 # some hook specific selectors #################################################
       
    80 
       
    81 @objectify_selector
       
    82 @lltrace
       
    83 def match_event(cls, req, **kwargs):
       
    84     if kwargs.get('event') in cls.events:
       
    85         return 1
       
    86     return 0
       
    87 
       
    88 @objectify_selector
       
    89 @lltrace
       
    90 def enabled_category(cls, req, **kwargs):
       
    91     if req is None:
       
    92         # server startup / shutdown event
       
    93         config = kwargs['repo'].config
       
    94     else:
       
    95         config = req.vreg.config
       
    96     if enabled_category in config.disabled_hooks_categories:
       
    97         return 0
       
    98     return 1
       
    99 
       
   100 @objectify_selector
       
   101 @lltrace
       
   102 def regular_session(cls, req, **kwargs):
       
   103     if req is None or req.is_super_session:
       
   104         return 0
       
   105     return 1
       
   106 
       
   107 class match_rtype(match_search_state):
       
   108     """accept if parameters specified as initializer arguments are specified
       
   109     in named arguments given to the selector
       
   110 
       
   111     :param *expected: parameters (eg `basestring`) which are expected to be
       
   112                       found in named arguments (kwargs)
       
   113     """
       
   114 
       
   115     @lltrace
       
   116     def __call__(self, cls, req, *args, **kwargs):
       
   117         return kwargs.get('rtype') in self.expected
       
   118 
       
   119 
       
   120 # base class for hook ##########################################################
       
   121 
       
   122 class Hook(AppObject):
       
   123     __registry__ = 'hooks'
       
   124     __select__ = match_event() & enabled_category()
       
   125     # set this in derivated classes
       
   126     events = None
       
   127     category = None
       
   128     order = 0
       
   129     # XXX deprecates
       
   130     enabled = True
       
   131 
       
   132     @classproperty
       
   133     def __id__(cls):
       
   134         warn('[3.5] %s: please specify an id for your hook' % cls)
       
   135         return str(id(cls))
       
   136 
       
   137     @classmethod
       
   138     def __registered__(cls, vreg):
       
   139         super(Hook, cls).__registered__(vreg)
       
   140         if getattr(cls, 'accepts', None):
       
   141             warn('[3.5] %s: accepts is deprecated, define proper __select__' % cls)
       
   142             rtypes = []
       
   143             for ertype in cls.accepts:
       
   144                 if ertype.islower():
       
   145                     rtypes.append(ertype)
       
   146                 else:
       
   147                     cls.__select__ = cls.__select__ & entity_implements(ertype)
       
   148             if rtypes:
       
   149                 cls.__select__ = cls.__select__ & match_rtype(*rtypes)
       
   150         return cls
       
   151 
       
   152     known_args = set(('entity', 'rtype', 'eidfrom', 'eidto', 'repo', 'timestamp'))
       
   153     def __init__(self, req, event, **kwargs):
       
   154         for arg in self.known_args:
       
   155             if arg in kwargs:
       
   156                 setattr(self, arg, kwargs.pop(arg))
       
   157         super(Hook, self).__init__(req, **kwargs)
       
   158         self.event = event
       
   159 
       
   160     def __call__(self):
       
   161         if hasattr(self, 'call'):
       
   162             warn('[3.5] %s: call is deprecated, implements __call__' % self.__class__)
       
   163             if self.event.endswith('_relation'):
       
   164                 self.call(self.cw_req, self.eidfrom, self.rtype, self.eidto)
       
   165             elif 'delete' in self.event:
       
   166                 self.call(self.cw_req, self.entity.eid)
       
   167             elif self.event.startswith('server_'):
       
   168                 self.call(self.repo)
       
   169             elif self.event.startswith('session_'):
       
   170                 self.call(self.cw_req)
       
   171             else:
       
   172                 self.call(self.cw_req, self.entity)
       
   173 
       
   174 set_log_methods(Hook, getLogger('cubicweb.hook'))
       
   175 
       
   176 
       
   177 # abstract classes for operation ###############################################
       
   178 
       
   179 class Operation(object):
       
   180     """an operation is triggered on connections pool events related to
       
   181     commit / rollback transations. Possible events are:
       
   182 
       
   183     precommit:
       
   184       the pool is preparing to commit. You shouldn't do anything things which
       
   185       has to be reverted if the commit fail at this point, but you can freely
       
   186       do any heavy computation or raise an exception if the commit can't go.
       
   187       You can add some new operation during this phase but their precommit
       
   188       event won't be triggered
       
   189 
       
   190     commit:
       
   191       the pool is preparing to commit. You should avoid to do to expensive
       
   192       stuff or something that may cause an exception in this event
       
   193 
       
   194     revertcommit:
       
   195       if an operation failed while commited, this event is triggered for
       
   196       all operations which had their commit event already to let them
       
   197       revert things (including the operation which made fail the commit)
       
   198 
       
   199     rollback:
       
   200       the transaction has been either rollbacked either
       
   201       * intentionaly
       
   202       * a precommit event failed, all operations are rollbacked
       
   203       * a commit event failed, all operations which are not been triggered for
       
   204         commit are rollbacked
       
   205 
       
   206     order of operations may be important, and is controlled according to:
       
   207     * operation's class
       
   208     """
       
   209 
       
   210     def __init__(self, session, **kwargs):
       
   211         self.session = session
       
   212         self.user = session.user
       
   213         self.repo = session.repo
       
   214         self.schema = session.repo.schema
       
   215         self.config = session.repo.config
       
   216         self.__dict__.update(kwargs)
       
   217         self.register(session)
       
   218         # execution information
       
   219         self.processed = None # 'precommit', 'commit'
       
   220         self.failed = False
       
   221 
       
   222     def register(self, session):
       
   223         session.add_operation(self, self.insert_index())
       
   224 
       
   225     def insert_index(self):
       
   226         """return the index of  the lastest instance which is not a
       
   227         LateOperation instance
       
   228         """
       
   229         for i, op in enumerate(self.session.pending_operations):
       
   230             if isinstance(op, (LateOperation, SingleLastOperation)):
       
   231                 return i
       
   232         return None
       
   233 
       
   234     def handle_event(self, event):
       
   235         """delegate event handling to the opertaion"""
       
   236         getattr(self, event)()
       
   237 
       
   238     def precommit_event(self):
       
   239         """the observed connections pool is preparing a commit"""
       
   240 
       
   241     def revertprecommit_event(self):
       
   242         """an error went when pre-commiting this operation or a later one
       
   243 
       
   244         should revert pre-commit's changes but take care, they may have not
       
   245         been all considered if it's this operation which failed
       
   246         """
       
   247 
       
   248     def commit_event(self):
       
   249         """the observed connections pool is commiting"""
       
   250 
       
   251     def revertcommit_event(self):
       
   252         """an error went when commiting this operation or a later one
       
   253 
       
   254         should revert commit's changes but take care, they may have not
       
   255         been all considered if it's this operation which failed
       
   256         """
       
   257 
       
   258     def rollback_event(self):
       
   259         """the observed connections pool has been rollbacked
       
   260 
       
   261         do nothing by default, the operation will just be removed from the pool
       
   262         operation list
       
   263         """
       
   264 
       
   265 set_log_methods(Operation, getLogger('cubicweb.session'))
       
   266 
       
   267 
       
   268 class LateOperation(Operation):
       
   269     """special operation which should be called after all possible (ie non late)
       
   270     operations
       
   271     """
       
   272     def insert_index(self):
       
   273         """return the index of  the lastest instance which is not a
       
   274         SingleLastOperation instance
       
   275         """
       
   276         for i, op in enumerate(self.session.pending_operations):
       
   277             if isinstance(op, SingleLastOperation):
       
   278                 return i
       
   279         return None
       
   280 
       
   281 
       
   282 class SingleOperation(Operation):
       
   283     """special operation which should be called once"""
       
   284     def register(self, session):
       
   285         """override register to handle cases where this operation has already
       
   286         been added
       
   287         """
       
   288         operations = session.pending_operations
       
   289         index = self.equivalent_index(operations)
       
   290         if index is not None:
       
   291             equivalent = operations.pop(index)
       
   292         else:
       
   293             equivalent = None
       
   294         session.add_operation(self, self.insert_index())
       
   295         return equivalent
       
   296 
       
   297     def equivalent_index(self, operations):
       
   298         """return the index of the equivalent operation if any"""
       
   299         equivalents = [i for i, op in enumerate(operations)
       
   300                        if op.__class__ is self.__class__]
       
   301         if equivalents:
       
   302             return equivalents[0]
       
   303         return None
       
   304 
       
   305 
       
   306 class SingleLastOperation(SingleOperation):
       
   307     """special operation which should be called once and after all other
       
   308     operations
       
   309     """
       
   310     def insert_index(self):
       
   311         return None