server/hook.py
branchstable
changeset 7677 134613d3b353
parent 7638 cc7cde77184f
child 7879 9aae456abab5
equal deleted inserted replaced
7670:6397a9051f65 7677:134613d3b353
    67 
    67 
    68 Operations
    68 Operations
    69 ~~~~~~~~~~
    69 ~~~~~~~~~~
    70 
    70 
    71 Operations are subclasses of the :class:`~cubicweb.server.hook.Operation` class
    71 Operations are subclasses of the :class:`~cubicweb.server.hook.Operation` class
    72 that may be created by hooks and scheduled to happen just before (or after) the
    72 that may be created by hooks and scheduled to happen on `precommit`,
    73 `precommit`, `postcommit` or `rollback` event. Hooks are being fired immediately
    73 `postcommit` or `rollback` event (i.e. respectivly before/after a commit or
    74 on data operations, and it is sometime necessary to delay the actual work down
    74 before a rollback of a transaction).
    75 to a time where all other hooks have run. Also while the order of execution of
    75 
    76 hooks is data dependant (and thus hard to predict), it is possible to force an
    76 Hooks are being fired immediately on data operations, and it is sometime
    77 order on operations.
    77 necessary to delay the actual work down to a time where we can expect all
       
    78 information to be there, or when all other hooks have run (though take case
       
    79 since operations may themselves trigger hooks). Also while the order of
       
    80 execution of hooks is data dependant (and thus hard to predict), it is possible
       
    81 to force an order on operations.
       
    82 
       
    83 So, for such case where you may miss some information that may be set later in
       
    84 the transaction, you should instantiate an operation in the hook.
    78 
    85 
    79 Operations may be used to:
    86 Operations may be used to:
    80 
    87 
    81 * implements a validation check which needs that all relations be already set on
    88 * implements a validation check which needs that all relations be already set on
    82   an entity
    89   an entity
   246 
   253 
   247 from warnings import warn
   254 from warnings import warn
   248 from logging import getLogger
   255 from logging import getLogger
   249 from itertools import chain
   256 from itertools import chain
   250 
   257 
   251 from logilab.common.decorators import classproperty
   258 from logilab.common.decorators import classproperty, cached
   252 from logilab.common.deprecation import deprecated, class_renamed
   259 from logilab.common.deprecation import deprecated, class_renamed
   253 from logilab.common.logging_ext import set_log_methods
   260 from logilab.common.logging_ext import set_log_methods
   254 
   261 
   255 from cubicweb import RegistryNotFound
   262 from cubicweb import RegistryNotFound
   256 from cubicweb.vregistry import classid
   263 from cubicweb.vregistry import classid
   257 from cubicweb.cwvreg import CWRegistry, VRegistry
   264 from cubicweb.cwvreg import CWRegistry, VRegistry
   258 from cubicweb.selectors import (objectify_selector, lltrace, ExpectedValueSelector,
   265 from cubicweb.selectors import (objectify_selector, lltrace, ExpectedValueSelector,
   259                                 is_instance)
   266                                 is_instance)
   260 from cubicweb.appobject import AppObject
   267 from cubicweb.appobject import AppObject, NotSelector, OrSelector
   261 from cubicweb.server.session import security_enabled
   268 from cubicweb.server.session import security_enabled
   262 
   269 
   263 ENTITIES_HOOKS = set(('before_add_entity',    'after_add_entity',
   270 ENTITIES_HOOKS = set(('before_add_entity',    'after_add_entity',
   264                       'before_update_entity', 'after_update_entity',
   271                       'before_update_entity', 'after_update_entity',
   265                       'before_delete_entity', 'after_delete_entity'))
   272                       'before_delete_entity', 'after_delete_entity'))
   316                 entities = []
   323                 entities = []
   317                 eids_from_to = kwargs.pop('eids_from_to')
   324                 eids_from_to = kwargs.pop('eids_from_to')
   318             else:
   325             else:
   319                 entities = []
   326                 entities = []
   320                 eids_from_to = []
   327                 eids_from_to = []
       
   328             pruned = self.get_pruned_hooks(session, event,
       
   329                                            entities, eids_from_to, kwargs)
   321             # by default, hooks are executed with security turned off
   330             # by default, hooks are executed with security turned off
   322             with security_enabled(session, read=False):
   331             with security_enabled(session, read=False):
   323                 for _kwargs in _iter_kwargs(entities, eids_from_to, kwargs):
   332                 for _kwargs in _iter_kwargs(entities, eids_from_to, kwargs):
   324                     hooks = sorted(self.possible_objects(session, **_kwargs),
   333                     hooks = sorted(self.filtered_possible_objects(pruned, session, **_kwargs),
   325                                    key=lambda x: x.order)
   334                                    key=lambda x: x.order)
   326                     with security_enabled(session, write=False):
   335                     with security_enabled(session, write=False):
   327                         for hook in hooks:
   336                         for hook in hooks:
   328                             #print hook.category, hook.__regid__
   337                            hook()
   329                             hook()
   338 
       
   339     def get_pruned_hooks(self, session, event, entities, eids_from_to, kwargs):
       
   340         """return a set of hooks that should not be considered by filtered_possible objects
       
   341 
       
   342         the idea is to make a first pass over all the hooks in the
       
   343         registry and to mark put some of them in a pruned list. The
       
   344         pruned hooks are the one which:
       
   345 
       
   346         * are disabled at the session level
       
   347         * have a match_rtype or an is_instance selector which does not
       
   348           match the rtype / etype of the relations / entities for
       
   349           which we are calling the hooks. This works because the
       
   350           repository calls the hooks grouped by rtype or by etype when
       
   351           using the entities or eids_to_from keyword arguments
       
   352 
       
   353         Only hooks with a simple selector or an AndSelector of simple
       
   354         selectors are considered for disabling.
       
   355 
       
   356         """
       
   357         if 'entity' in kwargs:
       
   358             entities = [kwargs['entity']]
       
   359         if len(entities):
       
   360             look_for_selector = is_instance
       
   361             etype = entities[0].__regid__
       
   362         elif 'rtype' in kwargs:
       
   363             look_for_selector = match_rtype
       
   364             etype = None
       
   365         else: # nothing to prune, how did we get there ???
       
   366             return set()
       
   367         cache_key = (event, kwargs.get('rtype'), etype)
       
   368         pruned = session.pruned_hooks_cache.get(cache_key)
       
   369         if pruned is not None:
       
   370             return pruned
       
   371         pruned = set()
       
   372         session.pruned_hooks_cache[cache_key] = pruned
       
   373         if look_for_selector is not None:
       
   374             for id, hooks in self.iteritems():
       
   375                 for hook in hooks:
       
   376                     enabled_cat, main_filter = hook.filterable_selectors()
       
   377                     if enabled_cat is not None:
       
   378                         if not enabled_cat(hook, session):
       
   379                             pruned.add(hook)
       
   380                             continue
       
   381                     if main_filter is not None:
       
   382                         if isinstance(main_filter, match_rtype) and \
       
   383                            (main_filter.frometypes is not None  or \
       
   384                             main_filter.toetypes is not None):
       
   385                             continue
       
   386                         first_kwargs = _iter_kwargs(entities, eids_from_to, kwargs).next()
       
   387                         if not main_filter(hook, session, **first_kwargs):
       
   388                             pruned.add(hook)
       
   389         return pruned
       
   390 
       
   391 
       
   392     def filtered_possible_objects(self, pruned, *args, **kwargs):
       
   393         for appobjects in self.itervalues():
       
   394             if pruned:
       
   395                 filtered_objects = [obj for obj in appobjects if obj not in pruned]
       
   396                 if not filtered_objects:
       
   397                     continue
       
   398             else:
       
   399                 filtered_objects = appobjects
       
   400             obj = self._select_best(filtered_objects,
       
   401                                     *args, **kwargs)
       
   402             if obj is None:
       
   403                 continue
       
   404             yield obj
   330 
   405 
   331 class HooksManager(object):
   406 class HooksManager(object):
   332     def __init__(self, vreg):
   407     def __init__(self, vreg):
   333         self.vreg = vreg
   408         self.vreg = vreg
   334 
   409 
   462     # XXX deprecated
   537     # XXX deprecated
   463     enabled = True
   538     enabled = True
   464     # stop pylint from complaining about missing attributes in Hooks classes
   539     # stop pylint from complaining about missing attributes in Hooks classes
   465     eidfrom = eidto = entity = rtype = None
   540     eidfrom = eidto = entity = rtype = None
   466 
   541 
       
   542     @classmethod
       
   543     @cached
       
   544     def filterable_selectors(cls):
       
   545         search = cls.__select__.search_selector
       
   546         if search((NotSelector, OrSelector)):
       
   547             return None, None
       
   548         enabled_cat = search(enabled_category)
       
   549         main_filter = search((is_instance, match_rtype))
       
   550         return enabled_cat, main_filter
   467 
   551 
   468     @classmethod
   552     @classmethod
   469     def check_events(cls):
   553     def check_events(cls):
   470         try:
   554         try:
   471             for event in cls.events:
   555             for event in cls.events:
   651     takes a session object as first argument (accessible as `.session` from the
   735     takes a session object as first argument (accessible as `.session` from the
   652     operation instance), and optionally all keyword arguments needed by the
   736     operation instance), and optionally all keyword arguments needed by the
   653     operation. These keyword arguments will be accessible as attributes from the
   737     operation. These keyword arguments will be accessible as attributes from the
   654     operation instance.
   738     operation instance.
   655 
   739 
   656     An operation is triggered on connections pool events related to
   740     An operation is triggered on connections set events related to commit /
   657     commit / rollback transations. Possible events are:
   741     rollback transations. Possible events are:
   658 
   742 
   659     * `precommit`:
   743     * `precommit`:
   660 
   744 
   661       the transaction is being prepared for commit. You can freely do any heavy
   745       the transaction is being prepared for commit. You can freely do any heavy
   662       computation, raise an exception if the commit can't go. or even add some
   746       computation, raise an exception if the commit can't go. or even add some
   726                  % classid(self.__class__), DeprecationWarning)
   810                  % classid(self.__class__), DeprecationWarning)
   727             self.commit_event()
   811             self.commit_event()
   728         getattr(self, event)()
   812         getattr(self, event)()
   729 
   813 
   730     def precommit_event(self):
   814     def precommit_event(self):
   731         """the observed connections pool is preparing a commit"""
   815         """the observed connections set is preparing a commit"""
   732 
   816 
   733     def revertprecommit_event(self):
   817     def revertprecommit_event(self):
   734         """an error went when pre-commiting this operation or a later one
   818         """an error went when pre-commiting this operation or a later one
   735 
   819 
   736         should revert pre-commit's changes but take care, they may have not
   820         should revert pre-commit's changes but take care, they may have not
   737         been all considered if it's this operation which failed
   821         been all considered if it's this operation which failed
   738         """
   822         """
   739 
   823 
   740     def rollback_event(self):
   824     def rollback_event(self):
   741         """the observed connections pool has been rollbacked
   825         """the observed connections set has been rollbacked
   742 
   826 
   743         do nothing by default, the operation will just be removed from the pool
   827         do nothing by default
   744         operation list
       
   745         """
   828         """
   746 
   829 
   747     def postcommit_event(self):
   830     def postcommit_event(self):
   748         """the observed connections pool has committed"""
   831         """the observed connections set has committed"""
   749 
   832 
   750     @property
   833     @property
   751     @deprecated('[3.6] use self.session.user')
   834     @deprecated('[3.6] use self.session.user')
   752     def user(self):
   835     def user(self):
   753         return self.session.user
   836         return self.session.user
  1026     instead of giving them into the rql string.
  1109     instead of giving them into the rql string.
  1027     """
  1110     """
  1028     data_key = 'neweids'
  1111     data_key = 'neweids'
  1029 
  1112 
  1030     def rollback_event(self):
  1113     def rollback_event(self):
  1031         """the observed connections pool has been rollbacked,
  1114         """the observed connections set has been rollbacked,
  1032         remove inserted eid from repository type/source cache
  1115         remove inserted eid from repository type/source cache
  1033         """
  1116         """
  1034         try:
  1117         try:
  1035             self.session.repo.clear_caches(self.get_data())
  1118             self.session.repo.clear_caches(self.get_data())
  1036         except KeyError:
  1119         except KeyError:
  1040     """on commit of delete query, we have to remove from repository's
  1123     """on commit of delete query, we have to remove from repository's
  1041     type/source cache eids of entities deleted in that transaction.
  1124     type/source cache eids of entities deleted in that transaction.
  1042     """
  1125     """
  1043     data_key = 'pendingeids'
  1126     data_key = 'pendingeids'
  1044     def postcommit_event(self):
  1127     def postcommit_event(self):
  1045         """the observed connections pool has been rollbacked,
  1128         """the observed connections set has been rollbacked,
  1046         remove inserted eid from repository type/source cache
  1129         remove inserted eid from repository type/source cache
  1047         """
  1130         """
  1048         try:
  1131         try:
  1049             self.session.repo.clear_caches(self.get_data())
  1132             self.session.repo.clear_caches(self.get_data())
  1050         except KeyError:
  1133         except KeyError: